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

# How to Create the Signature

> Step-by-step guide to generating signatures by implementing the SHA-256 algorithm in code (Node.js, Python, PHP) or using Niobi's Signature Generation API.

## Overview

Niobi supports two methods for generating request signatures:

1. **In Code (Custom Implementation)**: Run our deterministic SHA-256 signing algorithm directly on your backend server. Recommended for production.
2. **Via the Niobi Signature Generation API (`POST /api/niobi-signature`)**: Call Niobi's helper endpoint to automatically compute and return the signed payload.

<Info>
  **Important: `params` contains whatever the destination endpoint requires**\
  In both methods, the inner `"params": { ... }` object is dynamic. It is not limited to a fixed set of fields; it must contain **whatever specific parameters are required by the target API endpoint** you are invoking (e.g., Collections parameters for collections or Payout parameters for disbursements).
</Info>

***

<h2 id="method-1-in-code">
  Method 1: The Request Signing Algorithm (In Code)
</h2>

For production environments where you want to minimize network latency and generate signatures entirely within your backend infrastructure, implement the following 10-step algorithm:

1. **Obtain Credentials**: Log into your Niobi Dashboard, navigate to [**Workspace -> Integrations**](/getting-credentials), and create an integration entity. Note your `client_id`, `sender` (integration title), and `senderKey` (Secret Key).
2. **Assemble the Pre-Signing Object**: Create a JSON object containing your target endpoint parameters inside a nested `params` object, along with your metadata fields:

   | Field       | Requirement                   | Description                                                         |
   | :---------- | :---------------------------- | :------------------------------------------------------------------ |
   | `client_id` | **Required**                  | Your integration Client ID from the dashboard.                      |
   | `sender`    | **Required**                  | Your exact integration title from the dashboard.                    |
   | `senderKey` | **Required for signing only** | Your private Secret Key (must be deleted before HTTP transmission). |
   | `timestamp` | **Required**                  | The UNIX timestamp (in seconds) at the time the request is signed.  |
   | `salt`      | **Required**                  | Random entropy string generated on your server.                     |
   | `params`    | **Required**                  | Object holding the target endpoint's parameter payload.             |

   ```json theme={null}
   {
     "sender": "Integration_title",
     "timestamp": 1724835600,
     "salt": "random_salt_value",
     "client_id": "your_client_key_here",
     "senderKey": "your_secret_key_here",
     "params": {
       "amount": 10000,
       "currency": "KES",
       "country_id": 1,
       "mobile": "254161166649",
       "payment_method_type": "send money",
       "callback_url": "https://yourdomain.com/niobi/callback",
       "third_party_reference_1": "REF-001",
       "third_party_reference_2": "REF-002"
     }
   }
   ```
3. **Add a Salt (`salt`)**: Provide your own random string (for example, 16 to 32 alphanumeric characters). This is not provided by Niobi; it is an additional security and entropy layer you control on your server to guarantee that every payment request produces a unique signature.
4. **Include Client ID & Secret Key**: Set `"client_id"` with your integration's Client ID. **Temporarily** add your private Secret Key as `"senderKey"`.
5. **Add Integration Title (`sender`)**: Pass the exact integration name registered in your dashboard as `"sender"`.
6. **Sort Alphabetically (Recursive K-Sort)**: Sort all keys alphabetically in ascending order, applying the sort recursively for nested objects (such as `params`).
7. **Stringify Key-Value Pairs**: Convert the sorted structure into a single string formatted as `key=value`, concatenated with `&`. Nested properties are represented using dot notation (e.g. `params.amount=10000`).
   <Note>
     **Important URL Formatting**: The `callback_url` (or `client_callback_url`) must **not** be URL-encoded in the stringified hash representation. For example:
     `...&params.callback_url=https://yourdomain.com/niobi/callback&...`
   </Note>
8. **Hash the String with SHA-256**: Apply the SHA-256 hashing algorithm to the concatenated string. This produces a 64-character hexadecimal hash string.
9. **Attach the Signature**: Insert the resulting hash into the root JSON object as the `"signature"` field.
10. **Remove the Secret Key (`senderKey`)**: Delete the `"senderKey"` field from the payload completely before dispatching the request over the network.

<Warning>
  **Watch Out for Whitespace & Trailing Spaces:**\
  Ensure that your credentials (`client_id`, `senderKey`, `sender`) and string parameter values do not contain inadvertent leading or trailing whitespace, newlines, or extra spaces. A single accidental trailing space when copying a key will alter the computed hash and cause your request to fail with a `403 Request was not verified` error. We recommend trimming all string values before hashing.
</Warning>

***

## Detailed Payload Walkthrough

### 1. Before Signing (Pre-Signature Payload)

```json theme={null}
{
  "sender": "Integration_title",
  "timestamp": 1724835600,
  "salt": "random_salt_value",
  "client_id": "your_client_key_here",
  "senderKey": "your_secret_key_here",
  "params": {
    "amount": 10,
    "city": "Nairobi",
    "client_callback_url": "https://your-domain.com/niobi/result",
    "country": "KEN",
    "currency": "KES"
  }
}
```

### 2. Flattened, Sorted and Stringified Format

```
client_id=your_client_key_here&params.amount=10&params.city=Nairobi&params.client_callback_url=https://your-domain.com/niobi/result&params.country=KEN&params.currency=KES&salt=random_salt_value&sender=Integration_title&senderKey=your_secret_key_here&timestamp=1724835600
```

### 3. Final Signed Request Payload (Ready to Send to the Matching API Endpoint)

```json theme={null}
{
  "sender": "Integration_title",
  "timestamp": 1724835600,
  "salt": "random_salt_value",
  "client_id": "your_client_key_here",
  "params": {
    "amount": 10,
    "city": "Nairobi",
    "client_callback_url": "https://your-domain.com/niobi/result",
    "country": "KEN",
    "currency": "KES"
  },
  "signature": "219d12d1b0b9c8e66e84ee08cefafe05eaad68745a9e82cbcbc153131333c640"
}
```

***

## Implementation Code Examples

<Tabs>
  <Tab title="Node.js / TypeScript">
    ```javascript theme={null}
    const crypto = require('crypto');

    function recursiveSort(value) {
      if (Array.isArray(value)) {
        return value.map(recursiveSort);
      }
      if (value !== null && typeof value === 'object') {
        const sorted = {};
        for (const key of Object.keys(value).sort()) {
          sorted[key] = recursiveSort(value[key]);
        }
        return sorted;
      }
      return value;
    }

    function stringifyValue(value) {
      if (typeof value === 'boolean') return value ? '1' : '';
      if (value === null || value === undefined) return '';
      if (typeof value === 'string') return value.trim();
      return String(value);
    }

    function convertToQueryString(value, parentKey = null) {
      let entries;
      if (Array.isArray(value)) {
        entries = value.map((v, i) => [i, v]);
      } else if (value !== null && typeof value === 'object') {
        entries = Object.entries(value);
      } else {
        return stringifyValue(value);
      }

      const parts = entries.map(([key, subValue]) => {
        const currentKey = parentKey !== null ? `${parentKey}.${key}` : String(key);
        if (subValue !== null && typeof subValue === 'object') {
          return convertToQueryString(subValue, currentKey);
        }
        return `${currentKey}=${stringifyValue(subValue)}`;
      });

      return parts.join('&');
    }

    function signRequest(payload, secretKey) {
      // 1. Create a clone and inject senderKey temporarily
      const prePayload = {
        ...payload,
        senderKey: secretKey.trim()
      };

      // 2. Sort keys recursively, then flatten into the key=value string
      const sortedPayload = recursiveSort(prePayload);
      const queryString = convertToQueryString(sortedPayload);

      // 3. Compute SHA-256 hash
      const signature = crypto
        .createHash('sha256')
        .update(queryString)
        .digest('hex');

      // 4. Return final payload without senderKey
      const finalPayload = { ...payload, signature };
      return finalPayload;
    }

    // Example Usage: params contains whatever is needed for your target endpoint.
    // Method arrays (e.g. sendmoney) are flattened by index, not JSON-stringified:
    // this becomes params.sendmoney.0.phone_number=254647647649.
    const requestPayload = {
      client_id: "YOUR_CLIENT_ID",
      sender: "MyIntegration",
      salt: "random_salt_12345",
      timestamp: Math.floor(Date.now() / 1000),
      params: {
        amount: 10000,
        currency: "KES",
        country_id: 1,
        payment_method_type: "send money",
        callback_url: "https://example.com/callback",
        third_party_reference_1: "REF-001",
        third_party_reference_2: "REF-002",
        sendmoney: [
          { phone_number: "254647647649" }
        ]
      }
    };

    const signedPayload = signRequest(requestPayload, "YOUR_SECRET_KEY");
    console.log(JSON.stringify(signedPayload, null, 2));
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hashlib
    import json
    import time


    def recursive_ksort(value):
        if isinstance(value, dict):
            return {k: recursive_ksort(value[k]) for k in sorted(value.keys())}
        if isinstance(value, list):
            return [recursive_ksort(v) for v in value]
        return value


    def stringify_value(value) -> str:
        if isinstance(value, bool):
            return '1' if value else ''
        if value is None:
            return ''
        if isinstance(value, str):
            return value.strip()
        return str(value)


    def convert_to_query_string(value, parent_key=None) -> str:
        if isinstance(value, dict):
            items = list(value.items())
        elif isinstance(value, list):
            items = list(enumerate(value))
        else:
            return stringify_value(value)

        parts = []
        for key, sub_value in items:
            current_key = f"{parent_key}.{key}" if parent_key is not None else str(key)
            if isinstance(sub_value, (dict, list)):
                parts.append(convert_to_query_string(sub_value, current_key))
            else:
                parts.append(f"{current_key}={stringify_value(sub_value)}")
        return "&".join(parts)


    def sign_request(payload: dict, secret_key: str) -> dict:
        pre_payload = dict(payload)
        pre_payload["senderKey"] = secret_key.strip()

        # Sort keys recursively, then flatten into the key=value string
        sorted_payload = recursive_ksort(pre_payload)
        query_string = convert_to_query_string(sorted_payload)

        # Compute SHA-256 hash
        signature = hashlib.sha256(query_string.encode('utf-8')).hexdigest()

        # Build final payload without senderKey
        final_payload = dict(payload)
        final_payload["signature"] = signature
        return final_payload

    # Example Usage: params contains whatever is needed for your target endpoint.
    # Method arrays (e.g. sendmoney) are flattened by index, not JSON-encoded:
    # this becomes params.sendmoney.0.phone_number=254647647649.
    payload = {
        "client_id": "YOUR_CLIENT_ID",
        "sender": "MyIntegration",
        "salt": "random_salt_12345",
        "timestamp": int(time.time()),
        "params": {
            "amount": 10000,
            "currency": "KES",
            "country_id": 1,
            "payment_method_type": "send money",
            "callback_url": "https://example.com/callback",
            "third_party_reference_1": "REF-001",
            "third_party_reference_2": "REF-002",
            "sendmoney": [
                {"phone_number": "254647647649"}
            ]
        }
    }

    signed_request = sign_request(payload, "YOUR_SECRET_KEY")
    print(json.dumps(signed_request, indent=2))
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    <?php

    function recursiveKsort(array &$array) {
        ksort($array);
        foreach ($array as &$value) {
            if (is_array($value)) {
                recursiveKsort($value);
            }
        }
    }

    function stringifyValue($value): string {
        if (is_bool($value)) return $value ? '1' : '';
        if (is_null($value)) return '';
        if (is_string($value)) return trim($value);
        return (string) $value;
    }

    function convertToQueryString(array $array, ?string $parentKey = null): string {
        $parts = [];
        foreach ($array as $key => $value) {
            $currentKey = $parentKey !== null ? $parentKey . '.' . $key : (string) $key;
            if (is_array($value)) {
                $parts[] = convertToQueryString($value, $currentKey);
            } else {
                $parts[] = $currentKey . '=' . stringifyValue($value);
            }
        }
        return implode('&', $parts);
    }

    function signRequest(array $payload, string $secretKey): array {
        $prePayload = $payload;
        $prePayload['senderKey'] = trim($secretKey);

        recursiveKsort($prePayload);
        $queryString = convertToQueryString($prePayload);

        $signature = hash('sha256', $queryString);

        $finalPayload = $payload;
        $finalPayload['signature'] = $signature;

        return $finalPayload;
    }

    // Example Usage: params contains whatever is needed for your target endpoint.
    // Method arrays (e.g. sendmoney) are flattened by index, not JSON-encoded:
    // this becomes params.sendmoney.0.phone_number=254647647649.
    $requestPayload = [
        'client_id' => 'YOUR_CLIENT_ID',
        'sender' => 'MyIntegration',
        'salt' => 'random_salt_12345',
        'timestamp' => time(),
        'params' => [
            'amount' => 10000,
            'currency' => 'KES',
            'country_id' => 1,
            'payment_method_type' => 'send money',
            'callback_url' => 'https://example.com/callback',
            'third_party_reference_1' => 'REF-001',
            'third_party_reference_2' => 'REF-002',
            'sendmoney' => [
                ['phone_number' => '254647647649']
            ]
        ]
    ];

    $signed = signRequest($requestPayload, 'YOUR_SECRET_KEY');
    echo json_encode($signed, JSON_PRETTY_PRINT);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    import java.nio.charset.StandardCharsets;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.util.*;

    public class NiobiSignature {

        private static String stringifyValue(Object value) {
            if (value == null) return "";
            if (value instanceof Boolean) return ((Boolean) value) ? "1" : "";
            if (value instanceof String) return ((String) value).trim();
            return String.valueOf(value);
        }

        @SuppressWarnings("unchecked")
        private static String convertToQueryString(Object value, String parentKey) {
            List<Map.Entry<String, Object>> entries = new ArrayList<>();

            if (value instanceof Map) {
                List<String> keys = new ArrayList<>(((Map<String, Object>) value).keySet());
                Collections.sort(keys);
                for (String key : keys) {
                    entries.add(new AbstractMap.SimpleEntry<>(key, ((Map<String, Object>) value).get(key)));
                }
            } else if (value instanceof List) {
                List<Object> list = (List<Object>) value;
                for (int i = 0; i < list.size(); i++) {
                    entries.add(new AbstractMap.SimpleEntry<>(String.valueOf(i), list.get(i)));
                }
            } else {
                return stringifyValue(value);
            }

            List<String> parts = new ArrayList<>();
            for (Map.Entry<String, Object> entry : entries) {
                String currentKey = (parentKey == null || parentKey.isEmpty())
                    ? entry.getKey()
                    : parentKey + "." + entry.getKey();
                Object subValue = entry.getValue();

                if (subValue instanceof Map || subValue instanceof List) {
                    // Recurse into BOTH maps and lists the same way. An empty
                    // map/list contributes an empty string here, not "[]" or "key=".
                    parts.add(convertToQueryString(subValue, currentKey));
                } else {
                    parts.add(currentKey + "=" + stringifyValue(subValue));
                }
            }
            return String.join("&", parts);
        }

        public static String generateSignature(Map<String, Object> payload, String secretKey) throws NoSuchAlgorithmException {
            // 1. Clone payload and temporarily attach secret key
            Map<String, Object> prePayload = new LinkedHashMap<>(payload);
            prePayload.put("senderKey", secretKey.trim());

            // 2. Sort keys recursively, then flatten into the key=value string
            String queryString = convertToQueryString(prePayload, null);

            // 3. Compute SHA-256 hash
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(queryString.getBytes(StandardCharsets.UTF_8));

            StringBuilder hexString = new StringBuilder();
            for (byte b : hash) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) hexString.append('0');
                hexString.append(hex);
            }
            return hexString.toString();
        }

        public static void main(String[] args) throws Exception {
            // Method arrays (e.g. sendmoney) are flattened by index, not
            // stringified: this becomes params.sendmoney.0.phone_number=254647647649.
            Map<String, Object> sendMoneyEntry = new LinkedHashMap<>();
            sendMoneyEntry.put("phone_number", "254647647649");

            Map<String, Object> params = new LinkedHashMap<>();
            params.put("amount", 10000);
            params.put("currency", "KES");
            params.put("country_id", 1);
            params.put("payment_method_type", "send money");
            params.put("callback_url", "https://example.com/callback");
            params.put("third_party_reference_1", "REF-001");
            params.put("third_party_reference_2", "REF-002");
            params.put("sendmoney", Collections.singletonList(sendMoneyEntry));

            Map<String, Object> payload = new LinkedHashMap<>();
            payload.put("client_id", "YOUR_CLIENT_ID");
            payload.put("sender", "MyIntegration");
            payload.put("salt", "random_salt_12345");
            payload.put("timestamp", System.currentTimeMillis() / 1000L);
            payload.put("params", params);

            String signature = generateSignature(payload, "YOUR_SECRET_KEY");
            payload.put("signature", signature);

            System.out.println("Generated Signature: " + signature);
        }
    }
    ```
  </Tab>

  <Tab title="Go (Golang)">
    ```go theme={null}
    package main

    import (
    	"crypto/sha256"
    	"encoding/hex"
    	"encoding/json"
    	"fmt"
    	"sort"
    	"strconv"
    	"strings"
    	"time"
    )

    func stringifyValue(value interface{}) string {
    	if value == nil {
    		return ""
    	}
    	switch v := value.(type) {
    	case bool:
    		if v {
    			return "1"
    		}
    		return ""
    	case string:
    		return strings.TrimSpace(v)
    	default:
    		return fmt.Sprintf("%v", v)
    	}
    }

    func isContainer(value interface{}) bool {
    	switch value.(type) {
    	case map[string]interface{}, []interface{}:
    		return true
    	default:
    		return false
    	}
    }

    func convertToQueryString(value interface{}, parentKey string) string {
    	switch v := value.(type) {
    	case map[string]interface{}:
    		keys := make([]string, 0, len(v))
    		for k := range v {
    			keys = append(keys, k)
    		}
    		sort.Strings(keys)

    		parts := make([]string, 0, len(keys))
    		for _, k := range keys {
    			currentKey := k
    			if parentKey != "" {
    				currentKey = parentKey + "." + k
    			}
    			subValue := v[k]
    			if isContainer(subValue) {
    				// Recurse into BOTH maps and slices the same way. An empty
    				// map/slice contributes an empty string here, not "[]" or "key=".
    				parts = append(parts, convertToQueryString(subValue, currentKey))
    			} else {
    				parts = append(parts, currentKey+"="+stringifyValue(subValue))
    			}
    		}
    		return strings.Join(parts, "&")

    	case []interface{}:
    		parts := make([]string, 0, len(v))
    		for i, item := range v {
    			currentKey := strconv.Itoa(i)
    			if parentKey != "" {
    				currentKey = parentKey + "." + currentKey
    			}
    			if isContainer(item) {
    				parts = append(parts, convertToQueryString(item, currentKey))
    			} else {
    				parts = append(parts, currentKey+"="+stringifyValue(item))
    			}
    		}
    		return strings.Join(parts, "&")

    	default:
    		return stringifyValue(value)
    	}
    }

    func SignRequest(payload map[string]interface{}, secretKey string) string {
    	prePayload := make(map[string]interface{}, len(payload)+1)
    	for k, v := range payload {
    		prePayload[k] = v
    	}
    	prePayload["senderKey"] = strings.TrimSpace(secretKey)

    	// Sort keys recursively (inline, at each level) and flatten into the
    	// key=value string in the same pass.
    	queryString := convertToQueryString(prePayload, "")

    	hash := sha256.Sum256([]byte(queryString))
    	return hex.EncodeToString(hash[:])
    }

    func main() {
    	// Method arrays (e.g. sendmoney) are flattened by index, not
    	// JSON-marshaled: this becomes params.sendmoney.0.phone_number=254647647649.
    	params := map[string]interface{}{
    		"amount":              10000,
    		"currency":            "KES",
    		"country_id":          1,
    		"payment_method_type": "send money",
    		"callback_url":        "https://example.com/callback",
    		"third_party_reference_1": "REF-001",
    		"third_party_reference_2": "REF-002",
    		"sendmoney": []interface{}{
    			map[string]interface{}{"phone_number": "254647647649"},
    		},
    	}

    	payload := map[string]interface{}{
    		"client_id": "YOUR_CLIENT_ID",
    		"sender":    "MyIntegration",
    		"salt":      "random_salt_12345",
    		"timestamp": time.Now().Unix(),
    		"params":    params,
    	}

    	signature := SignRequest(payload, "YOUR_SECRET_KEY")
    	payload["signature"] = signature

    	out, _ := json.MarshalIndent(payload, "", "  ")
    	fmt.Println(string(out))
    }
    ```
  </Tab>

  <Tab title="C# / .NET">
    ```csharp theme={null}
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Cryptography;
    using System.Text;
    using System.Text.Json;

    public class NiobiSignature
    {
        private static string StringifyValue(object value)
        {
            if (value == null) return "";
            if (value is bool b) return b ? "1" : "";
            if (value is string s) return s.Trim();
            return Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture);
        }

        private static bool IsContainer(object value) =>
            value is IDictionary<string, object> || value is IEnumerable<object>;

        private static string ConvertToQueryString(object value, string parentKey)
        {
            if (value is IDictionary<string, object> dict)
            {
                var sortedKeys = dict.Keys.OrderBy(k => k, StringComparer.Ordinal).ToList();
                var parts = new List<string>();
                foreach (var key in sortedKeys)
                {
                    string currentKey = string.IsNullOrEmpty(parentKey) ? key : $"{parentKey}.{key}";
                    var subValue = dict[key];
                    // Recurse into BOTH dictionaries and lists the same way. An empty
                    // dictionary/list contributes an empty string, not "[]" or "key=".
                    parts.Add(IsContainer(subValue)
                        ? ConvertToQueryString(subValue, currentKey)
                        : $"{currentKey}={StringifyValue(subValue)}");
                }
                return string.Join("&", parts);
            }

            if (value is IEnumerable<object> list)
            {
                var parts = new List<string>();
                int i = 0;
                foreach (var item in list)
                {
                    string currentKey = string.IsNullOrEmpty(parentKey) ? i.ToString() : $"{parentKey}.{i}";
                    parts.Add(IsContainer(item)
                        ? ConvertToQueryString(item, currentKey)
                        : $"{currentKey}={StringifyValue(item)}");
                    i++;
                }
                return string.Join("&", parts);
            }

            return StringifyValue(value);
        }

        public static string GenerateSignature(Dictionary<string, object> payload, string secretKey)
        {
            var prePayload = new Dictionary<string, object>(payload)
            {
                ["senderKey"] = secretKey.Trim()
            };

            // Sort keys recursively (inline, at each level) and flatten into the
            // key=value string in the same pass.
            string queryString = ConvertToQueryString(prePayload, null);

            using var sha256 = SHA256.Create();
            byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(queryString));
            return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
        }

        public static void Main()
        {
            // Method arrays (e.g. sendmoney) are flattened by index, not
            // serialized: this becomes params.sendmoney.0.phone_number=254647647649.
            var paramsDict = new Dictionary<string, object>
            {
                { "amount", 10000 },
                { "currency", "KES" },
                { "country_id", 1 },
                { "payment_method_type", "send money" },
                { "callback_url", "https://example.com/callback" },
                { "third_party_reference_1", "REF-001" },
                { "third_party_reference_2", "REF-002" },
                { "sendmoney", new List<object> {
                    new Dictionary<string, object> { { "phone_number", "254647647649" } }
                } }
            };

            var payload = new Dictionary<string, object>
            {
                { "client_id", "YOUR_CLIENT_ID" },
                { "sender", "MyIntegration" },
                { "salt", "random_salt_12345" },
                { "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
                { "params", paramsDict }
            };

            string signature = GenerateSignature(payload, "YOUR_SECRET_KEY");
            payload["signature"] = signature;

            Console.WriteLine(JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }));
        }
    }
    ```
  </Tab>
</Tabs>

***

<h2 id="method-2-signature-api">
  Method 2: Generating Signatures via the Niobi Signature Generation API
</h2>

If you prefer not to write custom cryptographic sorting and hashing functions, or if you want to quickly test requests in Postman or Sandbox, Niobi provides a dedicated Signature Generation endpoint:

**Endpoint:** [`POST /api/niobi-signature`](/api-reference/authentication/generate-signature)

### Step 1: Send Your Request Parameters to the Signature Endpoint

Construct a request containing the following fields:

| Field       | Requirement  | Description                                                                                |
| :---------- | :----------- | :----------------------------------------------------------------------------------------- |
| `client_id` | **Required** | Your integration Client ID from the Niobi Dashboard.                                       |
| `sender`    | **Required** | Your exact integration title from the dashboard.                                           |
| `salt`      | **Required** | A unique random string generated on your server for entropy.                               |
| `params`    | **Required** | Object containing the specific parameters for the destination endpoint you intend to call. |

```json theme={null}
{
  "sender": "Integration_title",
  "salt": "random_salt_value",
  "client_id": "your_client_key_here",
  "params": {
    "amount": 10000,
    "mobile": "254161166649",
    "country_id": 1,
    "currency": "KES",
    "payment_method_type": "send money",
    "callback_url": "https://yourdomain.com/callback",
    "third_party_reference_1": "REF-001",
    "third_party_reference_2": "REF-002"
  }
}
```

### Step 2: Receive the Complete Signed Payload

Niobi validates your `client_id`, calculates the SHA-256 signature using your account's registered Secret Key, and returns the full signed envelope inside the `data` object:

```json theme={null}
{
  "success": true,
  "message": "Signature was generated successfully.",
  "data": {
    "client_id": "your_client_key_here",
    "params": {
      "amount": 10000,
      "mobile": "254161166649",
      "country_id": 1,
      "currency": "KES",
      "payment_method_type": "send money",
      "callback_url": "https://yourdomain.com/callback",
      "third_party_reference_1": "REF-001",
      "third_party_reference_2": "REF-002"
    },
    "salt": "random_salt_value",
    "sender": "Integration_title",
    "timestamp": 1724835600,
    "signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc"
  }
}
```

### Step 3: Pass the Signed Payload to the Target API

You can now take the exact JSON object inside `data` and send it directly to your target endpoint (e.g. `POST /api/v4/niobi-unified-collections` or `POST /api/v4/niobi-unified-payments`) **without changing or modifying anything as it is**.

<Warning>
  **Do Not Modify the Returned Payload:**\
  You must transmit the JSON object inside `data` exactly as returned, without adding, altering, or removing any fields. Any modification to parameters or values after generation will invalidate the signature and cause the target endpoint to reject your request with a `403 Request was not verified` error.
</Warning>

***

<h2 id="verifying-response-signatures">
  Verifying Response & Webhook Signatures
</h2>

When receiving a synchronous response or asynchronous webhook callback from Niobi, you can verify its authenticity using either code or the Verification API:

### 1. In Code (5 Steps):

1. **Extract Signature**: Store the `"signature"` field from the incoming JSON and remove it from the object.
2. **Add Secret Key**: Add `"senderKey": "your_secret_key_here"` into the object.
3. **Sort and Stringify**: Alphabetically sort keys recursively (K-sort) and format into `key=value` concatenated with `&`.
4. **Compute SHA-256 Hash**: Hash the string with SHA-256.
5. **Compare Signatures**: Compare your computed hash with the signature received in Step 1.

### 2. Via the Verification Helper API:

Send the payload to [`POST /api/niobi-verify`](/api-reference/authentication/verify-signature):

```json theme={null}
{
  "success": true,
  "message": "Request verified successfully."
}
```

***

## Next Steps

Now that you can generate valid signatures, proceed to construct the signed request envelope:

<div className="next-steps-flow">
  <a href="/authentication/passing-signature" className="next-step-card">
    <div className="next-step-badge">Step 2</div>
    <h3>How to Pass Signature in API Requests</h3>
    <p>Learn how to structure the final signed envelope, set required headers, and avoid common 403 errors.</p>
  </a>

  <div className="next-step-arrow">
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  </div>

  <a href="/collecting-payments/basics" className="next-step-card">
    <div className="next-step-badge">Integration</div>
    <h3>Start Collecting Payments</h3>
    <p>Send signed requests to the unified collection API to accept customer payments.</p>
  </a>
</div>
