# Introduction

Integrate deposit and withdrawal payment functionality into your applications with the Mangir Payment API.

Base URL https://payo.mangir.biz
🔒
JWT Authentication
Secure single-use tokens with 12-hour validity
💳
Two Deposit Methods
Payment Channel (Iframe) and Direct API
🔐
HMAC-SHA256 Callbacks
Signed notifications with signature verification
🔄
Retry Mechanism
5 automatic retry attempts for failed callbacks

Integration Flow

  1. Obtain API credentials (Public Key and Secret Key) from your merchant dashboard
  2. Authenticate to receive a JWT token
  3. Use the token to create deposit or withdrawal transactions
  4. Handle callback notifications for transaction status updates
Important All API requests must be made over HTTPS. HTTP requests will be rejected.

# Authentication

All API endpoints require JWT token authentication obtained with your merchant credentials.

POST /api/Account/Authenticate

Request Body

JSON
{
    "publicKey": "your-public-key",
    "secretKey": "your-secret-key"
}

Success Response

JSON
{
    "success": true,
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Error Response

JSON
{
    "success": false,
    "error": {
        "code": "AUTH_001",
        "message": "Invalid credentials"
    }
}

Token Rules

  • 12-hour validity: Tokens expire 12 hours after issuance.
  • Single-use after success: Tokens are consumed only after a successful Send operation.
  • Failed operations don't consume tokens: Validation errors, insufficient balance, etc. leave the token valid.
  • Server-side only: Store tokens securely on your server; never expose them to client-side code.
C#
public class AuthenticationService
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;

    public AuthenticationService(HttpClient httpClient, string baseUrl)
    {
        _httpClient = httpClient;
        _baseUrl = baseUrl;
    }

    public async Task<AuthResponse> AuthenticateAsync(string publicKey, string secretKey)
    {
        var request = new { publicKey, secretKey };
        var json = JsonSerializer.Serialize(request);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        var response = await _httpClient.PostAsync($"{_baseUrl}/api/Account/Authenticate", content);
        var body = await response.Content.ReadAsStringAsync();

        if (!response.IsSuccessStatusCode)
        {
            var error = JsonSerializer.Deserialize<ErrorResponse>(body);
            throw new ApiException(error?.Error?.Code ?? "UNKNOWN", error?.Error?.Message ?? "Authentication failed");
        }

        var result = JsonSerializer.Deserialize<AuthResponse>(body);
        if (result == null || !result.Success)
            throw new ApiException("AUTH_FAILED", "Authentication response was unsuccessful");

        return result;
    }
}

// Usage:
var httpClient = new HttpClient();
var authService = new AuthenticationService(httpClient, "https://payo.mangir.biz");
var auth = await authService.AuthenticateAsync("your-public-key", "your-secret-key");
httpClient.DefaultRequestHeaders.Add("token", auth.Token);
PHP
<?php

function authenticate(string $publicKey, string $secretKey): string
{
    $ch = curl_init('https://payo.mangir.biz/api/Account/Authenticate');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode([
            'publicKey' => $publicKey,
            'secretKey' => $secretKey
        ]),
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        throw new Exception("Authentication failed: HTTP $httpCode");
    }

    $data = json_decode($response, true);
    if (!$data['success']) {
        throw new Exception("Auth error: " . ($data['error']['message'] ?? 'Unknown'));
    }

    return $data['token'];
}

// Usage:
$token = authenticate('your-public-key', 'your-secret-key');
?>

# Deposit API

Two integration methods are available. Choose the one that best fits your application architecture.

OPTION 1 Payment Channel
1 Get JWT Token
2 Build Payment URL
3 Redirect user to URL
4 We handle the rest
OPTION 2 Direct API
1 Get JWT Token
2 GET /RandomBankAccounts
3 Show bank info in YOUR UI
4 POST /Deposit/Send
5 Transaction created

Payment Channel Integration

Redirect your user to our hosted payment page. We handle the entire payment UI including bank selection, confirmation, and waiting screen.

Payment URL Format

GET https://pay.mangir.biz/Deposit?token=...&fullName=...&username=...&amount=...&merchantOrderId=...

Parameters

ParameterTypeRequiredDescription
tokenstringREQUIREDJWT token from authentication
fullNamestringREQUIREDCustomer's full name (max 50 characters)
usernamestringREQUIREDCustomer's username or identifier (max 50 characters)
amountdecimalREQUIREDDeposit amount
merchantOrderIdstringOptionalYour internal order ID for tracking (max 100 characters)

Integration Flow

  1. Your server authenticates and gets a JWT token
  2. Your server builds the payment URL with customer details
  3. Customer is redirected to the payment page (or iframe is loaded)
  4. Customer selects a bank account from available options
  5. Customer makes the bank transfer and confirms on the payment page
  6. Our staff reviews and approves/rejects the transaction
  7. A callback is sent to your server with the result
C#
public async Task<string> GetPaymentUrlAsync(
    string publicKey, string secretKey,
    string fullName, string username, decimal amount, string merchantOrderId = null)
{
    var auth = await _authService.AuthenticateAsync(publicKey, secretKey);

    var queryParams = new Dictionary<string, string>
    {
        { "token", auth.Token },
        { "fullName", Uri.EscapeDataString(fullName) },
        { "username", Uri.EscapeDataString(username) },
        { "amount", amount.ToString("F2", CultureInfo.InvariantCulture) }
    };

    if (!string.IsNullOrEmpty(merchantOrderId))
        queryParams.Add("merchantOrderId", Uri.EscapeDataString(merchantOrderId));

    var qs = string.Join("&", queryParams.Select(p => $"{p.Key}={p.Value}"));
    return $"https://pay.mangir.biz/Deposit?{qs}";
}

// Usage in Controller:
var paymentUrl = await GetPaymentUrlAsync(publicKey, secretKey, "John Doe", "johndoe", 1000m, "ORDER-123");
return Redirect(paymentUrl);
// or return View with iframe: <iframe src="@paymentUrl" width="100%" height="600"></iframe>
PHP
<?php
$token = authenticate('your-public-key', 'your-secret-key');

$params = http_build_query([
    'token'    => $token,
    'fullName' => 'John Doe',
    'username' => 'johndoe',
    'amount'   => number_format(1000, 2, '.', ''),
    'merchantOrderId' => 'ORDER-123'
]);

$paymentUrl = "https://pay.mangir.biz/Deposit?{$params}";

// Option 1: Redirect
header("Location: $paymentUrl");

// Option 2: Iframe
// echo '<iframe src="' . htmlspecialchars($paymentUrl) . '" width="100%" height="600"></iframe>';
?>

Direct API Integration

Build your own payment UI and call our API directly from your backend. Full control over the user experience.

STEP 1 Get JWT Token
POST /api/Account/Authenticate

Authenticate with your credentials. See the Authentication section for details.

STEP 2 Get Available Bank Account
GET /api/Deposit/RandomBankAccounts?depositAmount=1000
Headers
HeaderValueRequired
tokenJWT token from Step 1REQUIRED
Query Parameters
ParameterTypeRequiredDescription
depositAmountdecimalREQUIREDThe deposit amount to filter eligible bank accounts
bankIdguidOptionalFilter by a specific bank ID
Response
JSON
{
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "bankId": "7a2e4c89-1234-5678-abcd-ef0123456789",
    "bankName": "Sample Bank",
    "accountHolder": "Account Holder Name",
    "iban": "TR330006100519786457841326",
    "minDeposit": 100.00,
    "maxDeposit": 10000.00
}
Note The token is NOT consumed at this step. You can call this endpoint multiple times with the same token.
STEP 3 Create Transaction
POST /api/Deposit/Send
Headers
HeaderValueRequired
tokenJWT token from Step 1REQUIRED
Request Body
JSON
{
    "userName": "johndoe",
    "fullName": "John Doe",
    "amount": 1000.00,
    "bankAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "notes": "Optional notes",
    "merchantOrderId": "MERCH-001"
}
Field Validations
FieldTypeRequiredMax LengthDescription
userNamestringREQUIRED50Customer's username or identifier
fullNamestringREQUIRED50Customer's full name
amountdecimalREQUIRED-Must be within bank account min/max limits
bankAccountIdguidREQUIRED-Bank account ID from Step 2
notesstringOptional500Optional notes for the transaction
merchantOrderIdstringOptional100Your internal order ID
Success Response
JSON
{
    "success": true,
    "message": "Deposit transaction initiated successfully",
    "transactionId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "bankAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "orderNo": "12345678",
    "merchantOrderId": "MERCH-001",
    "callbackUrl": "https://your-site.com/callback",
    "bankInfo": {
        "bankName": "Sample Bank",
        "accountHolder": "Account Holder Name",
        "iban": "TR330006100519786457841326",
        "minAmount": 100.00,
        "maxAmount": 10000.00
    }
}
Important The token IS consumed after a successful transaction creation. Authenticate again for the next transaction.
After Step 3 The transaction is created in our system. Staff will review and process it. The result will be sent to your callback URL automatically.
C#
public class DirectDepositService
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;
    private readonly string _publicKey;
    private readonly string _secretKey;

    public DirectDepositService(HttpClient httpClient, string baseUrl, string publicKey, string secretKey)
    {
        _httpClient = httpClient;
        _baseUrl = baseUrl;
        _publicKey = publicKey;
        _secretKey = secretKey;
    }

    public async Task<string> GetTokenAsync()
    {
        var request = new { publicKey = _publicKey, secretKey = _secretKey };
        var json = JsonSerializer.Serialize(request);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        var response = await _httpClient.PostAsync($"{_baseUrl}/api/Account/Authenticate", content);
        response.EnsureSuccessStatusCode();

        var body = await response.Content.ReadAsStringAsync();
        var result = JsonSerializer.Deserialize<JsonElement>(body);
        return result.GetProperty("token").GetString();
    }

    public async Task<BankAccountInfo> GetBankAccountAsync(string token, decimal amount)
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            $"{_baseUrl}/api/Deposit/RandomBankAccounts?depositAmount={amount}");
        request.Headers.Add("token", token);

        var response = await _httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();

        var body = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<BankAccountInfo>(body,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
    }

    public async Task<DepositResponse> CreateDepositAsync(string token, DepositRequest depositRequest)
    {
        var json = JsonSerializer.Serialize(depositRequest);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/api/Deposit/Send");
        request.Headers.Add("token", token);
        request.Content = content;

        var response = await _httpClient.SendAsync(request);
        var body = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<DepositResponse>(body,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
    }

    // Complete Flow
    public async Task<DepositResponse> ProcessDepositAsync(
        string userName, string fullName, decimal amount, string merchantOrderId = null)
    {
        var token = await GetTokenAsync();
        var bankAccount = await GetBankAccountAsync(token, amount);

        if (bankAccount == null)
            throw new Exception("No available bank account for the requested amount");

        return await CreateDepositAsync(token, new DepositRequest
        {
            UserName = userName,
            FullName = fullName,
            Amount = amount,
            BankAccountId = bankAccount.Id,
            MerchantOrderId = merchantOrderId
        });
    }
}

// Usage:
var service = new DirectDepositService(new HttpClient(), "https://payo.mangir.biz", "your-public-key", "your-secret-key");
var result = await service.ProcessDepositAsync("johndoe", "John Doe", 1000m, "ORDER-001");

if (result.Success)
    Console.WriteLine($"Order No: {result.OrderNo}, IBAN: {result.BankInfo.Iban}");
PHP
<?php

class DirectDepositService
{
    private string $baseUrl = 'https://payo.mangir.biz';
    private string $publicKey;
    private string $secretKey;

    public function __construct(string $publicKey, string $secretKey)
    {
        $this->publicKey = $publicKey;
        $this->secretKey = $secretKey;
    }

    public function getToken(): string
    {
        $response = $this->request('POST', '/api/Account/Authenticate', [
            'publicKey' => $this->publicKey,
            'secretKey' => $this->secretKey
        ]);
        return $response['token'];
    }

    public function getBankAccount(string $token, float $amount): array
    {
        return $this->request('GET', "/api/Deposit/RandomBankAccounts?depositAmount=$amount", null, ['token: ' . $token]);
    }

    public function createDeposit(string $token, array $data): array
    {
        return $this->request('POST', '/api/Deposit/Send', $data, ['token: ' . $token]);
    }

    public function processDeposit(string $userName, string $fullName, float $amount, ?string $merchantOrderId = null): array
    {
        $token = $this->getToken();
        $bank = $this->getBankAccount($token, $amount);

        return $this->createDeposit($token, [
            'userName' => $userName, 'fullName' => $fullName,
            'amount' => $amount, 'bankAccountId' => $bank['id'],
            'merchantOrderId' => $merchantOrderId
        ]);
    }

    private function request(string $method, string $path, ?array $data = null, array $headers = []): array
    {
        $ch = curl_init($this->baseUrl . $path);
        $allHeaders = array_merge(['Content-Type: application/json'], $headers);
        $opts = [CURLOPT_HTTPHEADER => $allHeaders, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30];
        if ($method === 'POST') { $opts[CURLOPT_POST] = true; if ($data) $opts[CURLOPT_POSTFIELDS] = json_encode($data); }
        curl_setopt_array($ch, $opts);
        $response = curl_exec($ch);
        curl_close($ch);
        return json_decode($response, true) ?? [];
    }
}

// Usage:
$service = new DirectDepositService('your-public-key', 'your-secret-key');
$result = $service->processDeposit('johndoe', 'John Doe', 1000.00, 'ORDER-001');

if ($result['success']) {
    echo "Order: " . $result['orderNo'] . " | IBAN: " . $result['bankInfo']['iban'];
}
?>
Important Notes
  • All API calls must be made from your backend server, never from client-side JavaScript.
  • RandomBankAccounts does not consume the token. You can retry multiple times.
  • Send does consume the token on success. Authenticate again for the next transaction.
  • Always validate the deposit amount falls within the bank's minDeposit / maxDeposit range.
  • Store the orderNo for tracking and reconciliation.

# Withdraw API

Create withdrawal transactions for your customers. Withdrawals are processed by staff and sent to the customer's bank account.

Step 1: Get Bank List

GET /api/Withdraw/Banks
HeaderValueRequired
tokenJWT tokenREQUIRED
Response
JSON
[
    {
        "id": "7a2e4c89-1234-5678-abcd-ef0123456789",
        "bankName": "Ziraat Bankasi",
        "bankCode": "0010"
    },
    {
        "id": "8b3f5d90-2345-6789-bcde-f01234567890",
        "bankName": "Garanti BBVA",
        "bankCode": "0062"
    }
]

Step 2: Create Withdrawal

POST /api/Withdraw/Send
HeaderValueRequired
tokenJWT tokenREQUIRED
Request Body
JSON
{
    "userName": "johndoe",
    "fullName": "John Doe",
    "iban": "TR330006100519786457841326",
    "amount": 500.00,
    "bankId": "7a2e4c89-1234-5678-abcd-ef0123456789",
    "notes": "Optional notes",
    "merchantOrderId": "WITHDRAW-001"
}
Field Validations
FieldTypeRequiredMaxDescription
userNamestringREQUIRED50Customer's username
fullNamestringREQUIRED50Must match bank account holder
ibanstringREQUIRED34Turkish format: TR + 24 digits
amountdecimalREQUIRED-Must not exceed merchant balance
bankIdguidREQUIRED-Bank ID from bank list
notesstringOptional500Optional notes
merchantOrderIdstringOptional100Your internal order reference
Success Response
JSON
{
    "success": true,
    "message": "Withdrawal request created successfully",
    "orderNo": "87654321"
}
Error Response
JSON
{
    "success": false,
    "error": {
        "code": "BAL_001",
        "message": "Insufficient balance"
    }
}
C#
public class WithdrawService
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;

    public WithdrawService(HttpClient httpClient, string baseUrl)
    {
        _httpClient = httpClient;
        _baseUrl = baseUrl;
    }

    public async Task<List<BankListItem>> GetBanksAsync(string token)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}/api/Withdraw/Banks");
        request.Headers.Add("token", token);
        var response = await _httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();
        var body = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<List<BankListItem>>(body,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
    }

    public async Task<WithdrawResponse> CreateWithdrawalAsync(string token, WithdrawRequest req)
    {
        var json = JsonSerializer.Serialize(req);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/api/Withdraw/Send");
        request.Headers.Add("token", token);
        request.Content = content;
        var response = await _httpClient.SendAsync(request);
        var body = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<WithdrawResponse>(body,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
    }
}
PHP
<?php
// Get bank list
function getBanks(string $token): array {
    $ch = curl_init('https://payo.mangir.biz/api/Withdraw/Banks');
    curl_setopt_array($ch, [
        CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'token: ' . $token],
        CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30
    ]);
    $response = curl_exec($ch); curl_close($ch);
    return json_decode($response, true) ?? [];
}

// Create withdrawal
function createWithdrawal(string $token, array $data): array {
    $ch = curl_init('https://payo.mangir.biz/api/Withdraw/Send');
    curl_setopt_array($ch, [
        CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data),
        CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'token: ' . $token],
        CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30
    ]);
    $response = curl_exec($ch); curl_close($ch);
    return json_decode($response, true) ?? [];
}

// Usage:
$banks = getBanks($token);
$result = createWithdrawal($token, [
    'userName' => 'johndoe', 'fullName' => 'John Doe',
    'iban' => 'TR330006100519786457841326', 'amount' => 500.00,
    'bankId' => $banks[0]['id'], 'merchantOrderId' => 'WITHDRAW-001'
]);
?>

# Callback System

HTTP POST notifications sent to your server when transaction status changes.

Status Codes

CodeStatusDescription
0PendingTransaction is waiting to be processed
1ReservedTransaction has been picked up by staff
2CompletedTransaction has been approved and completed
3RejectedTransaction has been rejected

Transaction Types

CodeType
1Deposit
2Withdrawal

Callback Payload

JSON
{
    "orderNo": "12345678",
    "merchantOrderId": "MERCH-001",
    "status": 2,
    "transactionType": 1,
    "amount": 1000.00,
    "message": "Transaction approved"
}

Callback Headers

HeaderDescription
Content-Typeapplication/json
X-Mangir-SignatureHMAC-SHA256 signature for verification
X-Mangir-TimestampUnix timestamp when callback was generated

Implementation Requirements

  • Your callback endpoint must accept POST requests
  • Return HTTP 200 OK to acknowledge receipt
  • Process the callback within 30 seconds
  • Verify the X-Mangir-Signature header for security
  • Implement idempotency: handle duplicate callbacks gracefully
  • Non-200 status codes trigger automatic retry
C#
[ApiController, Route("api/[controller]")]
public class CallbackController : ControllerBase
{
    private readonly string _secretKey;

    public CallbackController(IConfiguration config)
    {
        _secretKey = config["MangirApi:SecretKey"];
    }

    [HttpPost]
    public async Task<IActionResult> HandleCallback([FromBody] CallbackRequest request)
    {
        var signature = Request.Headers["X-Mangir-Signature"].FirstOrDefault();
        var timestamp = Request.Headers["X-Mangir-Timestamp"].FirstOrDefault();

        if (!VerifySignature(request, signature, timestamp))
            return Unauthorized("Invalid signature");

        switch (request.Status)
        {
            case 2: // Completed
                await ProcessCompletedTransaction(request);
                break;
            case 3: // Rejected
                await ProcessRejectedTransaction(request);
                break;
        }

        return Ok(new { success = true });
    }

    private bool VerifySignature(CallbackRequest request, string signature, string timestamp)
    {
        if (string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(timestamp)) return false;
        var signatureService = new SignatureService(_secretKey);
        return signatureService.VerifyCallback(request, signature, timestamp);
    }
}
PHP
<?php
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true);

$signature = $_SERVER['HTTP_X_MANGIR_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_MANGIR_TIMESTAMP'] ?? '';

// Verify signature
if (!verifySignature($data, $signature, $timestamp, 'your-secret-key')) {
    http_response_code(401);
    die(json_encode(['error' => 'Invalid signature']));
}

// Process
switch ($data['status'] ?? 0) {
    case 2: // Completed
        // markTransactionComplete($data['orderNo'], $data['amount']);
        break;
    case 3: // Rejected
        // markTransactionRejected($data['orderNo'], $data['message']);
        break;
}

http_response_code(200);
echo json_encode(['success' => true]);
?>

Retry Mechanism

If your endpoint does not return HTTP 200, we retry automatically:

AttemptDelayTotal Elapsed
1st (initial)Immediate0 minutes
2nd retry1 minute1 minute
3rd retry5 minutes6 minutes
4th retry15 minutes21 minutes
5th retry (final)30 minutes51 minutes
After 5 Failed Attempts The callback is marked as failed. You can view and manually retry failed callbacks from your merchant dashboard.

Testing Callbacks

POST /api/Callback/test-callback
JSON — Request
{
    "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "merchantOrderId": "TEST-001"
}
  • Test callbacks use the prefix TEST_ in the orderNo field
  • Test callbacks do not create actual transactions
  • Use this to verify your endpoint works before going live

# Callback Security

HMAC-SHA256 signature verification to ensure callback authenticity and prevent tampering.

Signature Headers

HeaderDescription
X-Mangir-SignatureBase64-encoded HMAC-SHA256 signature
X-Mangir-TimestampUnix timestamp (seconds since epoch)

Verification Steps

  1. Sort all callback JSON fields alphabetically by key name
  2. Join them as key1=value1&key2=value2&...
  3. Append the timestamp: dataString|timestamp
  4. Compute HMAC-SHA256 using your Secret Key
  5. Base64 encode the result
  6. Compare with the X-Mangir-Signature header

Step-by-Step Example

Given callback data:

JSON
{
    "orderNo": "12345678",
    "merchantOrderId": "MERCH-001",
    "status": 2,
    "transactionType": 1,
    "amount": 1000.00,
    "message": "Transaction approved"
}

Timestamp: 1704067200  |  Secret Key: your-secret-key

1. Sort alphabetically by key
amount, merchantOrderId, message, orderNo, status, transactionType
2. Join as key=value pairs
amount=1000.00&merchantOrderId=MERCH-001&message=Transaction approved&orderNo=12345678&status=2&transactionType=1
3. Append timestamp with pipe separator
amount=1000.00&merchantOrderId=MERCH-001&message=Transaction approved&orderNo=12345678&status=2&transactionType=1|1704067200
4-5. Compute HMAC-SHA256 and Base64 encode
X-Mangir-Signature: {base64-encoded-hmac-result}
C#
public class SignatureService
{
    private readonly string _secretKey;

    public SignatureService(string secretKey) { _secretKey = secretKey; }

    public string GenerateSignature(Dictionary<string, string> data, string timestamp)
    {
        var sortedPairs = data.OrderBy(kvp => kvp.Key).Select(kvp => $"{kvp.Key}={kvp.Value}");
        var dataString = string.Join("&", sortedPairs);
        var signatureBase = $"{dataString}|{timestamp}";

        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_secretKey));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureBase));
        return Convert.ToBase64String(hash);
    }

    public bool VerifyCallback(CallbackRequest request, string signature, string timestamp)
    {
        var data = new Dictionary<string, string>
        {
            { "orderNo", request.OrderNo },
            { "merchantOrderId", request.MerchantOrderId ?? "" },
            { "status", request.Status.ToString() },
            { "transactionType", request.TransactionType.ToString() },
            { "amount", request.Amount.ToString("F2", CultureInfo.InvariantCulture) },
            { "message", request.Message ?? "" }
        };

        var expected = GenerateSignature(data, timestamp);
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature));
    }
}
PHP
<?php
function verifySignature(array $data, string $signature, string $timestamp, string $secretKey): bool
{
    ksort($data);
    $pairs = [];
    foreach ($data as $key => $value) { $pairs[] = "$key=$value"; }
    $dataString = implode('&', $pairs);
    $signatureBase = "$dataString|$timestamp";
    $expected = base64_encode(hash_hmac('sha256', $signatureBase, $secretKey, true));
    return hash_equals($expected, $signature);
}
?>
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

public class SignatureService {
    private final String secretKey;

    public SignatureService(String secretKey) { this.secretKey = secretKey; }

    public String generateSignature(Map<String, String> data, String timestamp) throws Exception {
        String dataString = data.entrySet().stream()
            .sorted(Map.Entry.comparingByKey())
            .map(e -> e.getKey() + "=" + e.getValue())
            .collect(Collectors.joining("&"));

        String signatureBase = dataString + "|" + timestamp;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] hash = mac.doFinal(signatureBase.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hash);
    }

    public boolean verifyCallback(Map<String, String> data, String signature, String timestamp) throws Exception {
        String expected = generateSignature(data, timestamp);
        return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8));
    }
}

Common Mistakes

  • Not sorting alphabetically: Keys must be in ASCII alphabetical order
  • Number formatting: Use dot (.) as decimal separator with 2 decimal places (1000.00 not 1000)
  • Null values: Treat null/missing fields as empty strings ("")
  • Encoding: Use UTF-8 throughout
  • Timestamp validation: Reject callbacks older than 5 minutes to prevent replay attacks
  • String comparison: Always use constant-time comparison (hash_equals in PHP, CryptographicOperations.FixedTimeEquals in C#)

# Security Recommendations

Best practices for a secure integration.

  • HTTPS Only: All API communication must use HTTPS
  • Credential Storage: Store API keys in environment variables or a secure vault
  • IP Whitelisting: Configure allowed IPs in your merchant dashboard
  • Token Security: Never expose JWT tokens in client-side code, URLs, or logs
  • Input Validation: Validate all inputs before sending to the API
  • Callback Verification: Always verify HMAC-SHA256 signatures
  • Idempotency: Handle duplicate callbacks gracefully
  • Rate Limiting: Implement client-side rate limiting
  • Logging: Log API interactions but never log sensitive data
  • Error Handling: Don't expose internal error details to end users

Retry Strategy

Error TypeShould Retry?Codes
System errorsYesSYS_xxx
Validation errorsNoVAL_xxx
Bank errorsConditionalBANK_xxx
C#
public static HttpClient CreateSecureClient()
{
    var handler = new HttpClientHandler
    {
        SslProtocols = System.Security.Authentication.SslProtocols.Tls12 |
                       System.Security.Authentication.SslProtocols.Tls13
    };

    var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    return client;
}

# Error Codes

Consistent error format with machine-readable codes and human-readable messages.

Error Response Format

JSON
{
    "success": false,
    "error": {
        "code": "AUTH_001",
        "message": "Invalid credentials"
    }
}

Authentication Errors

CodeMessageDescription
AUTH_001Invalid credentialsPublic key or secret key is incorrect
AUTH_002Token expiredJWT token has expired (12-hour validity)
AUTH_003Token already usedSingle-use token has been consumed
VAL_002Missing tokenToken header not provided

Validation Errors

CodeMessageDescription
VAL_001Validation failedOne or more request fields are invalid
VAL_003Invalid amountAmount is outside acceptable range
VAL_004Invalid IBANIBAN format is invalid (withdraw only)
VAL_005Field too longString field exceeds maximum length
VAL_006Required field missingA required field is null or empty

Bank / Balance Errors

CodeMessageDescription
BANK_001No available bank accountNo bank account available for the requested amount
BANK_002Bank account not foundSpecified bank account ID does not exist
BANK_003Bank account inactiveSpecified bank account is currently inactive
BANK_004Invalid bankSpecified bank ID does not exist (withdraw only)
BAL_001Insufficient balanceMerchant balance insufficient for withdrawal

Transaction / System Errors

CodeMessageDescription
TRANS_001Transaction creation failedInternal error while creating transaction
SEC_001IP not allowedRequest from non-whitelisted IP
SEC_002Account suspendedMerchant account has been suspended
SYS_001Internal server errorUnexpected error — retry the request
C#
public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;

    public ErrorHandlingMiddleware(RequestDelegate next) { _next = next; }

    public async Task InvokeAsync(HttpContext context)
    {
        try { await _next(context); }
        catch (ApiException ex)
        {
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = ex.Code switch
            {
                "AUTH_001" or "AUTH_002" or "AUTH_003" or "VAL_002" => 401,
                "SEC_001" or "SEC_002" => 403,
                _ when ex.Code.StartsWith("VAL_") => 400,
                _ when ex.Code.StartsWith("BANK_") || ex.Code.StartsWith("BAL_") => 400,
                _ => 500
            };

            await context.Response.WriteAsJsonAsync(new {
                success = false, error = new { code = ex.Code, message = ex.Message }
            });
        }
        catch (Exception)
        {
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = 500;
            await context.Response.WriteAsJsonAsync(new {
                success = false, error = new { code = "SYS_001", message = "An internal server error occurred." }
            });
        }
    }
}

# MerchantOrderId Guide

Attach your internal order identifier to transactions for easy correlation and reconciliation.

  • merchantOrderId is optional but highly recommended
  • Maximum length: 100 characters
  • Returned in all callback notifications
  • Returned in transaction creation responses
  • Use for reconciliation and debugging

Usage in Payment Channel (URL Parameter)

URL
https://pay.mangir.biz/Deposit?token={token}&fullName=John+Doe&username=johndoe&amount=1000&merchantOrderId=ORDER-2026-001

Usage in Direct API (JSON Body)

JSON
{
    "userName": "johndoe",
    "fullName": "John Doe",
    "amount": 1000.00,
    "bankAccountId": "...",
    "merchantOrderId": "ORDER-2026-001"
}

In Callback Response

JSON
{
    "orderNo": "12345678",
    "merchantOrderId": "ORDER-2026-001",
    "status": 2,
    "transactionType": 1,
    "amount": 1000.00,
    "message": "Transaction approved"
}
C#
public class TransactionTracker
{
    private readonly ConcurrentDictionary<string, TransactionRecord> _transactions = new();

    public string CreateOrder(decimal amount, string customerId)
    {
        var merchantOrderId = $"ORD-{DateTime.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid().ToString("N")[..8]}";
        _transactions[merchantOrderId] = new TransactionRecord
        {
            MerchantOrderId = merchantOrderId,
            Amount = amount,
            CustomerId = customerId,
            Status = "Pending",
            CreatedAt = DateTime.UtcNow
        };
        return merchantOrderId;
    }

    public void UpdateFromCallback(string merchantOrderId, int status, string orderNo)
    {
        if (_transactions.TryGetValue(merchantOrderId, out var record))
        {
            record.OrderNo = orderNo;
            record.Status = status == 2 ? "Completed" : status == 3 ? "Rejected" : "Processing";
            record.UpdatedAt = DateTime.UtcNow;
        }
    }
}

public class TransactionRecord
{
    public string MerchantOrderId { get; set; }
    public string OrderNo { get; set; }
    public decimal Amount { get; set; }
    public string CustomerId { get; set; }
    public string Status { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
}

# Version History

v3.2.0 Current
  • Added Direct API as an officially supported integration method
  • Restructured documentation with tabbed Deposit section
  • Improved code examples for both Payment Channel and Direct API
  • Enhanced security documentation
v3.1.0
  • Added merchantOrderId support across all endpoints
  • MerchantOrderId returned in callback notifications
  • Updated API response models
v3.0.0
  • New deposit system with Payment Channel integration
  • Extended token validity to 12 hours
  • Improved bank account selection algorithm
  • New callback signature verification system
v2.0.0
  • Introduced single-use JWT tokens
  • Callback retry mechanism (5 attempts)
  • HMAC-SHA256 callback signatures
v1.5.0 Deprecated
  • Added Withdraw API
  • Bank list endpoint
  • IBAN validation
v1.0.0 Deprecated
  • Initial release
  • Basic deposit API
  • Simple authentication