Skip to main content

Overview

All API requests to Waypay payment endpoints must include a valid signature for security verification. This guide explains how to generate signatures correctly.

Quick Reference

PropertyValue
AlgorithmMD5
Output FormatLowercase hexadecimal (32 characters)
Secret Key Formatmer_sk_xxxxxxxxxxxxx (20 characters)
Key NormalizationAll keys converted to lowercase
Parameter SortingAlphabetical (ASCII order) on lowercase keys

Signature Generation Algorithm

Step-by-Step Process

  1. Collect all request parameters (excluding ‘signature’)
  2. Remove null values, empty strings, objects, and arrays
  3. Convert all parameter keys to LOWERCASE
  4. Sort parameters alphabetically by lowercase key name (ASCII order)
  5. Build query string: key1=value1&key2=value2&...
  6. Append your secret key directly (no & prefix)
  7. Compute MD5 hash of the combined string
  8. Convert to lowercase hexadecimal

Visual Example

Request:
{
  "amount": 1000,
  "currency": "PKR",
  "orderRef": {
    "orderRef": "ORD123456"
  },
  "description": "Payment for order",
  "customerRef": { "name": "Ayesha" },  ✗ SKIPPED (object)
  "items": ["item1", "item2"],           ✗ SKIPPED (array)
  "signature": "..."                     ✗ SKIPPED (signature field)
}

Secret Key: mer_sk_abc123def456

Step 1-2: Filter parameters
  ✓ amount, currency, description, orderRef

Step 3: Normalize keys to lowercase
  ✓ amount → amount
  ✓ currency → currency
  ✓ description → description
  ✓ orderRef → orderref (lowercase!)

Step 4: Sort alphabetically (lowercase keys)
  ✓ amount, currency, description, orderref

Step 5: Build query string
  ✓ amount=1000&currency=PKR&description=Payment for order&orderref={"orderRef":"ORD123456"}

Step 6: Append secret key
  ✓ amount=1000&currency=PKR&description=Payment for order&orderref={"orderRef":"ORD123456"}mer_sk_abc123def456

Step 7-8: MD5 hash (lowercase)
  ✓ a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Value Formatting Rules

Critical: Format Values CorrectlyDifferent data types require specific formatting rules to ensure signature consistency.
TypeFormatExample InputSignature Value
StringAs-is"PKR"PKR
IntegerAs-is (plain number)100100
LongAs-is (plain number)10001000
Decimal.NET ToString()1000.501000.50
Double.NET ToString()99.9999.99
Float.NET ToString()50.550.5
BooleanLowercasetruetrue
EnumInteger valueWalletProvider.JazzCash (value: 2)2
DateTime.NET ToString()2024-01-15T10:30:001/15/2024 10:30:00 AM
DateTimeOffset.NET ToString()2024-01-15T10:30:00+05:001/15/2024 10:30:00 AM +05:00
NullSKIPnull(not included)
Empty StringSKIP""(not included)
ObjectSKIP{ "name": "..." }(not included)
ArraySKIP["a", "b"](not included)

Important Notes

All parameter keys must be converted to lowercase before sorting and building the query string:
  • orderReforderref
  • callbackUrlcallbackurl
  • paymentMethodpaymentmethod
This prevents signature mismatches due to casing differences.
  • Integers (int, long): Plain number → 100, 1000, 20
  • Decimals (decimal, double, float): Use .NET ToString() → 1000.50, 99.99
  • Enums: Convert to integer value → WalletProvider.JazzCash (2) → "2"
✅ Correct: true, false❌ Incorrect: True, FALSE, TRUE
  • Most objects (like customerRef): Excluded from signature calculation
  • Required parameter objects (like orderRef): Serialized to JSON string and included
  • Arrays: Always excluded from signature calculation
Example:
{
  "orderRef": { "orderRef": "ORD123456" },  // → included as JSON string
  "customerRef": { "name": "..." },          // → excluded
  "items": ["a", "b"]                        // → excluded
}
The format will vary based on culture settings.Recommendation: Use ISO 8601 strings for consistency across different systems.

Code Examples

using System.Security.Cryptography;
using System.Text;

public static string GenerateSignature(Dictionary<string, object> parameters, string secretKey)
{
    // Filter, normalize keys to lowercase, and sort parameters
    var sortedParams = new SortedDictionary<string, string>(StringComparer.Ordinal);
    
    foreach (var kvp in parameters)
    {
        // Skip signature field
        if (kvp.Key.Equals("signature", StringComparison.OrdinalIgnoreCase))
            continue;
            
        // Skip null values
        if (kvp.Value == null)
            continue;
            
        // Skip complex types (objects, arrays)
        if (IsComplexType(kvp.Value))
            continue;
            
        // Normalize key to lowercase and format value
        var normalizedKey = kvp.Key.ToLowerInvariant();
        sortedParams[normalizedKey] = FormatValue(kvp.Value);
    }

    // Build query string
    var queryString = string.Join("&", 
        sortedParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));

    // Append secret key and compute MD5
    var signatureInput = queryString + secretKey;
    
    using var md5 = MD5.Create();
    var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(signatureInput));
    
    return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}

private static bool IsComplexType(object value)
{
    if (value == null) return false;
    var type = value.GetType();
    
    // Check for arrays
    if (type.IsArray) return true;
    
    // Check for collections (but not string)
    if (typeof(System.Collections.IEnumerable).IsAssignableFrom(type) && type != typeof(string))
        return true;
    
    // Check for complex objects (classes, but not string or decimal)
    return type.IsClass && type != typeof(string) && !type.IsPrimitive && type != typeof(decimal);
}

private static string FormatValue(object value)
{
    return value switch
    {
        // Integers: plain number
        int i => i.ToString(),
        long l => l.ToString(),
        
        // Decimals: use ToString() (preserves precision)
        decimal d => d.ToString(),
        double dbl => dbl.ToString(),
        float f => f.ToString(),
        
        // Booleans: lowercase
        bool b => b.ToString().ToLowerInvariant(),
        
        // Enums: convert to integer
        Enum e => Convert.ToInt32(e).ToString(),
        
        // DateTime: default ToString()
        DateTime dt => dt.ToString(),
        DateTimeOffset dto => dto.ToString(),
        
        // Default: ToString()
        _ => value.ToString()
    };
}

Testing Your Implementation

Test Endpoint

Use our signature testing API to verify your implementation:
POST /api/v1/signature/generate
Request:
{
  "parameters": {
    "amount": 1000,
    "currency": "PKR",
    "orderRef": {
      "orderRef": "ORD123456"
    }
  },
  "secretKey": "mer_sk_abc123def456"
}
Response:
{
  "signature": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "queryString": "amount=1000&currency=PKR&orderref={\"orderRef\":\"ORD123456\"}",
  "sortedParameters": {
    "amount": "1000",
    "currency": "PKR",
    "orderref": "{\"orderRef\":\"ORD123456\"}"
  },
  "signatureInput": "amount=1000&currency=PKR&orderref={\"orderRef\":\"ORD123456\"}mer_sk_abc...456"
}

Verify Endpoint

POST /api/v1/signature/verify
Request:
{
  "parameters": {
    "amount": 1000,
    "currency": "PKR",
    "orderRef": {
      "orderRef": "ORD123456"
    }
  },
  "secretKey": "mer_sk_abc123def456",
  "providedSignature": "your_generated_signature"
}
Response (Success):
{
  "isValid": true,
  "providedSignature": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "expectedSignature": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "queryString": "amount=1000&currency=PKR&orderref={\"orderRef\":\"ORD123456\"}",
  "issues": []
}
Response (Failure):
{
  "isValid": false,
  "providedSignature": "wrong_signature",
  "expectedSignature": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "queryString": "amount=1000&currency=PKR&orderref={\"orderRef\":\"ORD123456\"}",
  "sortedParameters": {
    "amount": "1000",
    "currency": "PKR",
    "orderref": "{\"orderRef\":\"ORD123456\"}"
  },
  "issues": [
    "Signature length is 15, expected 32 characters for MD5",
    "Signature contains uppercase letters - should be lowercase"
  ]
}

Common Mistakes & Troubleshooting

Common Errors

ErrorCauseSolution
Signature length ≠ 32Wrong hash algorithm or encodingUse MD5, output as hex
Uppercase letters in signatureNot converting to lowercaseCall .toLowerCase() on result
Wrong parameter orderNot sorting alphabeticallyUse ASCII/lexicographic sort
Including signature fieldSignature included in paramsExclude ‘signature’ key
Wrong number formatIncorrect formattingUse plain numbers for integers
Including nested objectsObjects/arrays in signatureSkip complex types

Debugging Checklist

1

Is your signature 32 characters?

MD5 produces 32 hex characters. If shorter/longer, check your hash function.
2

Is your signature lowercase?

Must be abcdef123456... not ABCDEF123456...
3

Are all parameter keys converted to lowercase?

  • orderReforderref
  • callbackUrlcallbackurl
  • Amountamount
4

Are parameters sorted correctly?

Alphabetical (ASCII) order on lowercase keys. All keys must be lowercase before sorting.
5

Are numbers formatted correctly?

  • Integers: Plain number → 1000, 20, 5
  • Decimals: Use ToString() → 1000.50, 99.99
  • Enums: Integer value → 2, 5
6

Are you skipping objects and arrays?

  • customerRef: { name: "..." } → SKIP
  • orderRef: { orderRef: "..." } → INCLUDE (serialize to JSON string)
  • items: [...] → SKIP
7

Is the secret key appended correctly?

NO & before secret key. Directly append: ...orderId=123mer_sk_xxx

API Request Example

Complete Payment Request

curl -X POST "https://gateway.dev.waypay.live/Gateway/v1/Payment/initiate-checkout" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -d '{
    "amount": 1500,
    "currency": "PKR",
    "orderRef": {
      "orderRef": "ORD123456"
    },
    "description": "Payment for Order #2024-001",
    "callbackUrl": "https://yoursite.com/callback",
    "customerRef": {
      "name": "Ayesha Khan",
      "email": "ayesha@example.com"
    },
    "signature": "a1b2c3d4e5f6789012345678abcdef12"
  }'
Signature Calculation:
Parameters (after filtering, customerRef and orderRef are skipped as they're objects):
  amount=1500
  callbackUrl=https://yoursite.com/callback
  currency=PKR
  description=Payment for Order #2024-001

Keys normalized to lowercase:
  amount → amount
  callbackUrl → callbackurl
  currency → currency
  description → description

Query String (sorted by lowercase keys):
  amount=1500&callbackurl=https://yoursite.com/callback&currency=PKR&description=Payment for Order #2024-001

Signature Input:
  amount=1500&callbackurl=https://yoursite.com/callback&currency=PKR&description=Payment for Order #2024-001mer_sk_your_secret_key

MD5 Hash:
  a1b2c3d4e5f6789012345678abcdef12

Security Best Practices

Protect Your Secret Key

Never expose your secret key in client-side code or commit it to version control.
Don’t include in JavaScript, mobile apps, or browser code. Generate signatures on your server only.
  • Use environment variables
  • Use secret management services (Azure Key Vault, AWS Secrets Manager)
  • Never commit to version control
Contact support to regenerate your secret key and update all integrations with the new key.
All API calls must use HTTPS. Never send signatures over HTTP.

Secret Key Format

Format: mer_sk_xxxxxxxxxxxxx
Length: 20 characters
Prefix: mer_sk_ (7 characters)
Random: 13 alphanumeric characters (lowercase)

Example: mer_sk_a1b2c3d4e5f6g

Getting Help

Test Your Implementation

  1. Use /api/v1/signature/example to see a complete example
  2. Use /api/v1/signature/generate to generate signatures for testing
  3. Use /api/v1/signature/verify to debug signature mismatches

Contact Support

If you continue to have issues:
  • Email: support@waypay.com
  • Include: Your merchant ID, request payload (without signature), and the signature you generated

Migration from v1.1 to v2.0

Breaking Changes in v2.0If you have existing integrations, you must update your signature generation code.

Breaking Changes

Old: orderRef, callbackUrl, paymentMethodNew: orderref, callbackurl, paymentmethod
Old: 1000"1000.00"New: 1000"1000"
New: Enums are converted to integer valuesExample: WalletProvider.JazzCash (value: 2) → "2"
Old: ISO 8601 format "2024-01-15T10:30:00"New: .NET default ToString() (culture-dependent)

Migration Steps

1

Update your signature generation code

Convert all parameter keys to lowercase before sorting and building the query string.
2

Remove forced 2-decimal formatting for integers

Use plain numbers for integer values (e.g., 1000 instead of 1000.00).
3

Test your signatures

Use the /api/v1/signature/generate endpoint to verify your implementation.
4

Verify before deploying

Use /api/v1/signature/verify to validate signatures before deploying to production.

Changelog

VersionDateChanges
1.02024-01-15Initial release
1.12024-01-20Added: Skip objects and arrays from signature calculation
2.02026-01-26BREAKING: Keys normalized to lowercase; Integer formatting changed (plain numbers); Enum handling added; DateTime uses default ToString()

Document Version: 2.0
Last Updated: 2026-01-26
API Version: v1