> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waypay.live/llms.txt
> Use this file to discover all available pages before exploring further.

# Signature Generation Guide

> Learn how to generate secure signatures for Waypay API requests

## 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

| Property              | Value                                            |
| --------------------- | ------------------------------------------------ |
| **Algorithm**         | MD5                                              |
| **Output Format**     | Lowercase hexadecimal (32 characters)            |
| **Secret Key Format** | `mer_sk_xxxxxxxxxxxxx` (20 characters)           |
| **Key Normalization** | All keys converted to **lowercase**              |
| **Parameter Sorting** | Alphabetical (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

<Warning>
  **Critical: Format Values Correctly**

  Different data types require specific formatting rules to ensure signature consistency.
</Warning>

| Type               | Format               | Example Input                        | Signature Value                |
| ------------------ | -------------------- | ------------------------------------ | ------------------------------ |
| **String**         | As-is                | `"PKR"`                              | `PKR`                          |
| **Integer**        | As-is (plain number) | `100`                                | `100`                          |
| **Long**           | As-is (plain number) | `1000`                               | `1000`                         |
| **Decimal**        | .NET ToString()      | `1000.50`                            | `1000.50`                      |
| **Double**         | .NET ToString()      | `99.99`                              | `99.99`                        |
| **Float**          | .NET ToString()      | `50.5`                               | `50.5`                         |
| **Boolean**        | Lowercase            | `true`                               | `true`                         |
| **Enum**           | Integer value        | `WalletProvider.JazzCash` (value: 2) | `2`                            |
| **DateTime**       | .NET ToString()      | `2024-01-15T10:30:00`                | `1/15/2024 10:30:00 AM`        |
| **DateTimeOffset** | .NET ToString()      | `2024-01-15T10:30:00+05:00`          | `1/15/2024 10:30:00 AM +05:00` |
| **Null**           | **SKIP**             | `null`                               | *(not included)*               |
| **Empty String**   | **SKIP**             | `""`                                 | *(not included)*               |
| **Object**         | **SKIP**             | `{ "name": "..." }`                  | *(not included)*               |
| **Array**          | **SKIP**             | `["a", "b"]`                         | *(not included)*               |

### Important Notes

<AccordionGroup>
  <Accordion title="🔑 ALL Keys Must Be Lowercase">
    All parameter keys must be converted to lowercase before sorting and building the query string:

    * `orderRef` → `orderref`
    * `callbackUrl` → `callbackurl`
    * `paymentMethod` → `paymentmethod`

    This prevents signature mismatches due to casing differences.
  </Accordion>

  <Accordion title="Numbers: Format depends on type">
    * **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"`
  </Accordion>

  <Accordion title="Booleans: Must be lowercase">
    ✅ Correct: `true`, `false`

    ❌ Incorrect: `True`, `FALSE`, `TRUE`
  </Accordion>

  <Accordion title="Objects & Arrays: Handling varies by type">
    * **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:**

    ```javascript theme={null}
    {
      "orderRef": { "orderRef": "ORD123456" },  // → included as JSON string
      "customerRef": { "name": "..." },          // → excluded
      "items": ["a", "b"]                        // → excluded
    }
    ```
  </Accordion>

  <Accordion title="DateTime Values: Use .NET default ToString()">
    The format will vary based on culture settings.

    **Recommendation:** Use ISO 8601 strings for consistency across different systems.
  </Accordion>
</AccordionGroup>

## Code Examples

<CodeGroup>
  ```csharp C# theme={null}
  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()
      };
  }
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function generateSignature(parameters, secretKey) {
      // Filter and normalize parameters
      const filtered = Object.entries(parameters)
          .filter(([key, value]) => {
              if (key.toLowerCase() === 'signature') return false;
              if (value === null || value === undefined) return false;
              if (typeof value === 'object') return false; // Skip objects & arrays
              return true;
          })
          // Normalize keys to lowercase
          .map(([key, value]) => [key.toLowerCase(), value]);

      // Sort alphabetically by lowercase key
      filtered.sort((a, b) => a[0].localeCompare(b[0]));

      // Format values and build query string
      const queryString = filtered
          .map(([key, value]) => `${key}=${formatValue(value)}`)
          .join('&');

      // Append secret key and compute MD5
      const signatureInput = queryString + secretKey;
      return crypto.createHash('md5').update(signatureInput).digest('hex');
  }

  function formatValue(value) {
      if (typeof value === 'number') {
          // Check if it's an integer or has decimal places
          if (Number.isInteger(value)) {
              return value.toString(); // Plain number for integers
          }
          return value.toString(); // Keep decimal precision
      }
      if (typeof value === 'boolean') {
          return value.toString().toLowerCase();
      }
      return String(value);
  }

  // Usage
  const params = {
      amount: 1000,        // Integer: will be "1000"
      currency: 'PKR',
      orderRef: {
        orderRef: 'ORD123456'
      }
  };
  const signature = generateSignature(params, 'mer_sk_abc123def456');
  console.log(signature);
  ```

  ```python Python theme={null}
  import hashlib
  from decimal import Decimal

  def generate_signature(parameters: dict, secret_key: str) -> str:
      # Filter and normalize parameters
      filtered = {}
      for key, value in parameters.items():
          if key.lower() == 'signature':
              continue
          if value is None:
              continue
          if isinstance(value, (dict, list)):  # Skip objects & arrays
              continue
          
          # Normalize key to lowercase
          normalized_key = key.lower()
          filtered[normalized_key] = format_value(value)
      
      # Sort alphabetically by lowercase key
      sorted_params = dict(sorted(filtered.items()))
      
      # Build query string
      query_string = '&'.join([f'{k}={v}' for k, v in sorted_params.items()])
      
      # Append secret key and compute MD5
      signature_input = query_string + secret_key
      return hashlib.md5(signature_input.encode('utf-8')).hexdigest()

  def format_value(value):
      if isinstance(value, bool):
          # Handle bool before int (since bool is subclass of int in Python)
          return str(value).lower()
      if isinstance(value, int):
          # Integers: plain number
          return str(value)
      if isinstance(value, (float, Decimal)):
          # Decimals: use str() to preserve precision
          return str(value)
      return str(value)

  # Usage
  params = {
      'amount': 1000.00,
      'currency': 'PKR',
      'orderRef': {
          'orderRef': 'ORD123456'
      }
  }
  signature = generate_signature(params, 'mer_sk_abc123def456')
  print(signature)
  ```

  ```php PHP theme={null}
  <?php

  function generateSignature(array $parameters, string $secretKey): string {
      // Filter and normalize parameters
      $filtered = [];
      foreach ($parameters as $key => $value) {
          if (strtolower($key) === 'signature') continue;
          if ($value === null) continue;
          if (is_array($value) || is_object($value)) continue; // Skip objects & arrays
          
          // Normalize key to lowercase
          $normalizedKey = strtolower($key);
          $filtered[$normalizedKey] = formatValue($value);
      }
      
      // Sort alphabetically by lowercase key (ASCII order)
      ksort($filtered, SORT_STRING);
      
      // Build query string
      $parts = [];
      foreach ($filtered as $key => $value) {
          $parts[] = "$key=$value";
      }
      $queryString = implode('&', $parts);
      
      // Append secret key and compute MD5
      $signatureInput = $queryString . $secretKey;
      return strtolower(md5($signatureInput));
  }

  function formatValue($value): string {
      if (is_bool($value)) {
          // Handle bool before int check
          return $value ? 'true' : 'false';
      }
      if (is_int($value)) {
          // Integers: plain number
          return (string)$value;
      }
      if (is_float($value)) {
          // Floats: use string conversion to preserve precision
          return (string)$value;
      }
      return (string)$value;
  }

  // Usage
  $params = [
      'amount' => 1000.00,
      'currency' => 'PKR',
      'orderRef' => [
          'orderRef' => 'ORD123456'
      ]
  ];
  $signature = generateSignature($params, 'mer_sk_abc123def456');
  echo $signature;
  ?>
  ```

  ```java Java theme={null}
  import java.security.MessageDigest;
  import java.util.*;

  public class SignatureGenerator {
      
      public static String generateSignature(Map<String, Object> parameters, String secretKey) 
              throws Exception {
          
          // Filter, normalize keys, and sort parameters
          TreeMap<String, String> sorted = new TreeMap<>();
          
          for (Map.Entry<String, Object> entry : parameters.entrySet()) {
              String key = entry.getKey();
              Object value = entry.getValue();
              
              if (key.equalsIgnoreCase("signature")) continue;
              if (value == null) continue;
              if (value instanceof Map || value instanceof List) continue; // Skip objects & arrays
              
              // Normalize key to lowercase
              String normalizedKey = key.toLowerCase();
              sorted.put(normalizedKey, formatValue(value));
          }
          
          // Build query string
          StringBuilder queryString = new StringBuilder();
          for (Map.Entry<String, String> entry : sorted.entrySet()) {
              if (queryString.length() > 0) queryString.append("&");
              queryString.append(entry.getKey()).append("=").append(entry.getValue());
          }
          
          // Append secret key and compute MD5
          String signatureInput = queryString.toString() + secretKey;
          
          MessageDigest md = MessageDigest.getInstance("MD5");
          byte[] hashBytes = md.digest(signatureInput.getBytes("UTF-8"));
          
          StringBuilder hexString = new StringBuilder();
          for (byte b : hashBytes) {
              hexString.append(String.format("%02x", b));
          }
          
          return hexString.toString();
      }
      
      private static String formatValue(Object value) {
          if (value instanceof Boolean) {
              // Handle boolean before Integer check
              return value.toString().toLowerCase();
          }
          if (value instanceof Integer || value instanceof Long) {
              // Integers: plain number
              return value.toString();
          }
          if (value instanceof Double || value instanceof Float) {
              // Decimals: use toString() to preserve precision
              return value.toString();
          }
          if (value instanceof Enum) {
              // Enums: convert to integer value
              return String.valueOf(((Enum<?>) value).ordinal());
          }
          return value.toString();
      }
  }
  ```
</CodeGroup>

## Testing Your Implementation

### Test Endpoint

Use our signature testing API to verify your implementation:

```bash theme={null}
POST /api/v1/signature/generate
```

**Request:**

```json theme={null}
{
  "parameters": {
    "amount": 1000,
    "currency": "PKR",
    "orderRef": {
      "orderRef": "ORD123456"
    }
  },
  "secretKey": "mer_sk_abc123def456"
}
```

**Response:**

```json theme={null}
{
  "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

```bash theme={null}
POST /api/v1/signature/verify
```

**Request:**

```json theme={null}
{
  "parameters": {
    "amount": 1000,
    "currency": "PKR",
    "orderRef": {
      "orderRef": "ORD123456"
    }
  },
  "secretKey": "mer_sk_abc123def456",
  "providedSignature": "your_generated_signature"
}
```

**Response (Success):**

```json theme={null}
{
  "isValid": true,
  "providedSignature": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "expectedSignature": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "queryString": "amount=1000&currency=PKR&orderref={\"orderRef\":\"ORD123456\"}",
  "issues": []
}
```

**Response (Failure):**

```json theme={null}
{
  "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

| Error                          | Cause                            | Solution                        |
| ------------------------------ | -------------------------------- | ------------------------------- |
| Signature length ≠ 32          | Wrong hash algorithm or encoding | Use MD5, output as hex          |
| Uppercase letters in signature | Not converting to lowercase      | Call `.toLowerCase()` on result |
| Wrong parameter order          | Not sorting alphabetically       | Use ASCII/lexicographic sort    |
| Including signature field      | Signature included in params     | Exclude 'signature' key         |
| Wrong number format            | Incorrect formatting             | Use plain numbers for integers  |
| Including nested objects       | Objects/arrays in signature      | Skip complex types              |

### Debugging Checklist

<Steps>
  <Step title="Is your signature 32 characters?">
    MD5 produces 32 hex characters. If shorter/longer, check your hash function.
  </Step>

  <Step title="Is your signature lowercase?">
    Must be `abcdef123456...` not `ABCDEF123456...`
  </Step>

  <Step title="Are all parameter keys converted to lowercase?">
    * `orderRef` → `orderref`
    * `callbackUrl` → `callbackurl`
    * `Amount` → `amount`
  </Step>

  <Step title="Are parameters sorted correctly?">
    Alphabetical (ASCII) order on **lowercase keys**. All keys must be lowercase before sorting.
  </Step>

  <Step title="Are numbers formatted correctly?">
    * Integers: Plain number → `1000`, `20`, `5`
    * Decimals: Use ToString() → `1000.50`, `99.99`
    * Enums: Integer value → `2`, `5`
  </Step>

  <Step title="Are you skipping objects and arrays?">
    * `customerRef: { name: "..." }` → SKIP
    * `orderRef: { orderRef: "..." }` → INCLUDE (serialize to JSON string)
    * `items: [...]` → SKIP
  </Step>

  <Step title="Is the secret key appended correctly?">
    NO `&` before secret key. Directly append: `...orderId=123mer_sk_xxx`
  </Step>
</Steps>

## API Request Example

### Complete Payment Request

<CodeGroup>
  ```bash cURL theme={null}
  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"
    }'
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  // Payment parameters
  const params = {
    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'
    }
  };

  // Generate signature
  const signature = generateSignature(params, 'mer_sk_your_secret_key');

  // Make API request
  const response = await fetch('https://gateway.dev.waypay.live/Gateway/v1/Payment/initiate-checkout', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer YOUR_JWT_TOKEN'
    },
    body: JSON.stringify({ ...params, signature })
  });
  ```

  ```python Python theme={null}
  import requests
  import hashlib

  # Payment parameters
  params = {
      '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'
      }
  }

  # Generate signature
  signature = generate_signature(params, 'mer_sk_your_secret_key')

  # Make API request
  response = requests.post(
      'https://gateway.dev.waypay.live/Gateway/v1/Payment/initiate-checkout',
      headers={
          'Content-Type': 'application/json',
          'Authorization': 'Bearer YOUR_JWT_TOKEN'
      },
      json={**params, 'signature': signature}
  )
  ```
</CodeGroup>

**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

<Warning>
  Never expose your secret key in client-side code or commit it to version control.
</Warning>

<AccordionGroup>
  <Accordion title="Never expose in client-side code">
    Don't include in JavaScript, mobile apps, or browser code. Generate signatures on your server only.
  </Accordion>

  <Accordion title="Store securely">
    * Use environment variables
    * Use secret management services (Azure Key Vault, AWS Secrets Manager)
    * Never commit to version control
  </Accordion>

  <Accordion title="Rotate if compromised">
    Contact support to regenerate your secret key and update all integrations with the new key.
  </Accordion>

  <Accordion title="Use HTTPS only">
    All API calls must use HTTPS. Never send signatures over HTTP.
  </Accordion>
</AccordionGroup>

### 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](mailto:support@waypay.com)
* **Include:** Your merchant ID, request payload (without signature), and the signature you generated

## Migration from v1.1 to v2.0

<Warning>
  **Breaking Changes in v2.0**

  If you have existing integrations, you must update your signature generation code.
</Warning>

### Breaking Changes

<AccordionGroup>
  <Accordion title="1. All keys are now lowercase in signature calculation">
    **Old:** `orderRef`, `callbackUrl`, `paymentMethod`

    **New:** `orderref`, `callbackurl`, `paymentmethod`
  </Accordion>

  <Accordion title="2. Integer formatting changed">
    **Old:** `1000` → `"1000.00"`

    **New:** `1000` → `"1000"`
  </Accordion>

  <Accordion title="3. Enum handling">
    **New:** Enums are converted to integer values

    **Example:** `WalletProvider.JazzCash` (value: 2) → `"2"`
  </Accordion>

  <Accordion title="4. DateTime formatting">
    **Old:** ISO 8601 format `"2024-01-15T10:30:00"`

    **New:** .NET default ToString() (culture-dependent)
  </Accordion>
</AccordionGroup>

### Migration Steps

<Steps>
  <Step title="Update your signature generation code">
    Convert all parameter keys to lowercase before sorting and building the query string.
  </Step>

  <Step title="Remove forced 2-decimal formatting for integers">
    Use plain numbers for integer values (e.g., `1000` instead of `1000.00`).
  </Step>

  <Step title="Test your signatures">
    Use the `/api/v1/signature/generate` endpoint to verify your implementation.
  </Step>

  <Step title="Verify before deploying">
    Use `/api/v1/signature/verify` to validate signatures before deploying to production.
  </Step>
</Steps>

## Changelog

| Version | Date       | Changes                                                                                                                                       |
| ------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 1.0     | 2024-01-15 | Initial release                                                                                                                               |
| 1.1     | 2024-01-20 | Added: Skip objects and arrays from signature calculation                                                                                     |
| 2.0     | 2026-01-26 | **BREAKING**: 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
