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

# Signature Generation Guide

> Sign deposit and withdrawal requests with an MD5 signature over the request fields and your signing secret, so forged or tampered orders are rejected before any money moves.

## Overview

Your API key identifies you. The **request signature** proves that an order request was created by someone holding your signing secret and that its fields were not changed on the way. C2C verifies the signature **before** it validates the request or runs any business logic. A request with a missing, malformed or incorrect signature is rejected with `401`, and no order is created.

The signature is a field named `signature` **inside the JSON request body**.

<Info>
  Your **signing secret** (`sksec_...`) is issued together with your API key and is shown **only once**. Each API key has its own signing secret. Sign a request with the secret that belongs to the API key in that request's `C2C-API-Key` header.
</Info>

## Which endpoints require a signature

Signatures are required on requests that move money. Read-only requests are authenticated by your API key alone.

| Endpoint | Signature |
| - | - |
| [Create deposit order](/api-reference/endpoints/create-deposit) `POST /api/merchant/orders` | **Required.** Unsigned or incorrectly signed requests are always rejected. |
| [Create withdrawal order](/api-reference/endpoints/create-withdrawal) `POST /api/merchant/orders` | **Required.** |
| [Query order status](/api-reference/endpoints/query-order-status) `GET /api/merchant/orders/{id}/status` or `GET /api/merchant/orders/status?merchantOrderRef={ref}` | Not used. API key only. |
| [Query wallet balance](/api-reference/endpoints/query-balance) `GET /api/merchant/orders/balance` | Not used. API key only. |

## Quick reference

| Property | Value |
| - | - |
| **Algorithm** | MD5 |
| **Output format** | 32 lowercase hexadecimal characters |
| **Where it goes** | The `signature` field of the JSON request body |
| **Signed fields** | Every top-level field of the body except `signature`. Fields that are `null`, empty strings, objects or arrays are skipped. |
| **Key normalization** | Keys are converted to **lowercase** |
| **Sort order** | Ascending ASCII order of the lowercase keys |
| **String to hash** | `key1=value1&key2=value2&...` followed **directly** by your signing secret (no `&`, no separator) |
| **Secret format** | `sksec_` followed by 40 alphanumeric characters |
| **Maximum body size** | 64 KB (larger bodies are rejected with `413`) |

## Signature generation algorithm

<Steps>
  <Step title="Collect the fields you will send">
    Take every top-level field of the request body except `signature`.
  </Step>

  <Step title="Drop what isn't signed">
    Remove fields whose value is `null` or an empty string `""`, and fields whose value is an object or an array.
  </Step>

  <Step title="Lowercase the keys">
    `orderType` → `ordertype`, `merchantOrderRef` → `merchantorderref`, `callbackUrl` → `callbackurl`.
  </Step>

  <Step title="Sort by key">
    Sort the fields by their lowercase key in ascending ASCII (byte) order.
  </Step>

  <Step title="Build the query string">
    Join the fields as `key=value` pairs with `&`. Values are used as-is: no URL encoding and no JSON quoting.
  </Step>

  <Step title="Append your signing secret">
    Concatenate the secret directly to the end of the query string, with nothing in between.
  </Step>

  <Step title="Hash">
    Compute the MD5 of the UTF-8 bytes of that string and write it as 32 lowercase hex characters.
  </Step>

  <Step title="Send it">
    Add the result to the body as `"signature": "<hex>"` and send the body.
  </Step>
</Steps>

### Visual example

```text theme={null}
Request body (before signing):
{
  "orderType": "Deposit",
  "amount": 1500,
  "currency": "PKR",
  "walletType": "JazzCash",
  "merchantOrderRef": "INV-10045",
  "customerReference": "player_12345",
  "callbackUrl": "https://merchant.example.com/webhooks/c2c",
  "metadata": null                        ✗ skipped (null)
}

Signing secret: sksec_Q9mTz4Lk8XwR2bN6vH1cJ7pY3sF5gD0aE8uK2iOq   (example only)

Steps 1–3 – keep the signed fields, lowercase the keys
  ordertype=Deposit, amount=1500, currency=PKR, wallettype=JazzCash,
  merchantorderref=INV-10045, customerreference=player_12345,
  callbackurl=https://merchant.example.com/webhooks/c2c

Steps 4–5 – sort by key and join
  amount=1500&callbackurl=https://merchant.example.com/webhooks/c2c&currency=PKR&customerreference=player_12345&merchantorderref=INV-10045&ordertype=Deposit&wallettype=JazzCash

Step 6 – append the secret
  amount=1500&callbackurl=https://merchant.example.com/webhooks/c2c&currency=PKR&customerreference=player_12345&merchantorderref=INV-10045&ordertype=Deposit&wallettype=JazzCashsksec_Q9mTz4Lk8XwR2bN6vH1cJ7pY3sF5gD0aE8uK2iOq

Step 7 – MD5, lowercase hex
  56eff91b5ab0351471a2a8d457320230

Step 8 – body as sent
  {"orderType":"Deposit","amount":1500,"currency":"PKR","walletType":"JazzCash","merchantOrderRef":"INV-10045","customerReference":"player_12345","callbackUrl":"https://merchant.example.com/webhooks/c2c","signature":"56eff91b5ab0351471a2a8d457320230"}
```

## Value formatting rules

<Warning>
  A value is signed **exactly as it appears in the JSON you send**. If you build the body from an object, sign the same object you serialize.
</Warning>

| JSON value | Signed as | Example in body | Signed value |
| - | - | - | - |
| String | The string itself, unescaped | `"currency": "PKR"` | `PKR` |
| String with special characters | As-is, no URL encoding | `"callbackUrl": "https://shop.example/cb?a=1&b=2"` | `https://shop.example/cb?a=1&b=2` |
| Whole number | As written | `"amount": 1500` | `1500` |
| Decimal number | As written, trailing zeros included | `"amount": 1500.50` | `1500.50` |
| Boolean | Lowercase | `true` | `true` |
| Enum (`orderType`, `walletType`) | As written | `"walletType": "JazzCash"` or `"walletType": 0` | `JazzCash` or `0` |
| `null` | **Skipped** | `"metadata": null` | *(not included)* |
| Empty string | **Skipped** | `"callbackUrl": ""` | *(not included)* |
| Object or array | **Skipped** | `"extra": {"a": 1}` | *(not included)* |

<AccordionGroup>
  <Accordion title="Numbers: 1500, 1500.5 and 1500.50 are different values">
    The number is signed as written in the body, so `1500` signs as `1500` and `1500.50` signs as `1500.50`. Most JSON libraries write `1500.5` for a floating-point `1500.50`. That's fine as long as the value you sign comes from the same object you serialize. Don't use exponent notation such as `1.5e3`.
  </Accordion>

  <Accordion title="Optional fields">
    Include or omit optional fields as you like, but sign what you send. Omitting a field and sending it as `null` produce the same signature. Every top-level field you send is signed, including fields C2C doesn't use, so don't add fields after signing.
  </Accordion>

  <Accordion title="Key casing">
    Keys are lowercased before sorting, so `callbackUrl` and `CallbackURL` both sign as `callbackurl`. A body that contains the same key twice in different cases (for example `amount` and `Amount`) is rejected with `SIGNATURE_MALFORMED`.
  </Accordion>

  <Accordion title="metadata">
    `metadata` is a string. If it contains JSON, sign the string exactly as you send it, for example `metadata={"cart":"A1"}`.
  </Accordion>
</AccordionGroup>

## Worked examples

Use these inputs to check your implementation. All three use the example signing secret `sksec_Q9mTz4Lk8XwR2bN6vH1cJ7pY3sF5gD0aE8uK2iOq` *(example only, not a real credential)*.

| Request | Body (without `signature`) | Expected `signature` |
| - | - | - |
| Deposit | `{"orderType":"Deposit","amount":1500,"currency":"PKR","walletType":"JazzCash","merchantOrderRef":"INV-10045","customerReference":"player_12345","callbackUrl":"https://merchant.example.com/webhooks/c2c"}` | `56eff91b5ab0351471a2a8d457320230` |
| Withdrawal | `{"orderType":"Withdrawal","amount":2000,"currency":"PKR","walletType":"Easypaisa","merchantOrderRef":"PAYOUT-20017","customerReference":"player_12345","customerAccountNumber":"03001234567","callbackUrl":"https://merchant.example.com/webhooks/c2c"}` | `8dddc4abc8aa78744bb29624ac583434` |
| Deposit with a decimal amount, a `null` and an integer option | `{"orderType":"Deposit","amount":1500.5,"currency":"PKR","walletType":"JazzCash","merchantOrderRef":"INV-10046","customerReference":"player_12345","callbackUrl":"https://merchant.example.com/webhooks/c2c","metadata":null,"txnRefDigitsRequired":6}` | `e07d0c722ae8f9085825390797fd0219` |

The query strings for the second and third examples are:

```text theme={null}
amount=2000&callbackurl=https://merchant.example.com/webhooks/c2c&currency=PKR&customeraccountnumber=03001234567&customerreference=player_12345&merchantorderref=PAYOUT-20017&ordertype=Withdrawal&wallettype=Easypaisa

amount=1500.5&callbackurl=https://merchant.example.com/webhooks/c2c&currency=PKR&customerreference=player_12345&merchantorderref=INV-10046&ordertype=Deposit&txnrefdigitsrequired=6&wallettype=JazzCash
```

<Note>
  These worked examples are checked by C2C's automated test suite against the production verification code.
</Note>

## Code examples

Each example signs a payload and sends a deposit order. The helper signs the **same object** that is serialized into the body.

<CodeGroup>
  ```csharp C# theme={null}
  using System.Net.Http;
  using System.Security.Cryptography;
  using System.Text;
  using System.Text.Json;
  using System.Text.Json.Nodes;

  public static class C2CSigner
  {
      /// <summary>Adds the "signature" field to the payload and returns the JSON string to send.</summary>
      public static string SignBody(JsonObject payload, string signingSecret)
      {
          using var document = JsonDocument.Parse(payload.ToJsonString());
          var parameters = new SortedDictionary<string, string>(StringComparer.Ordinal);   // ASCII order

          foreach (var property in document.RootElement.EnumerateObject())
          {
              if (property.Name.Equals("signature", StringComparison.OrdinalIgnoreCase))
                  continue;

              var value = property.Value.ValueKind switch
              {
                  JsonValueKind.String => property.Value.GetString(),
                  JsonValueKind.Number => property.Value.GetRawText(),   // exactly as written in the body
                  JsonValueKind.True => "true",
                  JsonValueKind.False => "false",
                  _ => null,                                             // null, objects and arrays are not signed
              };
              if (!string.IsNullOrEmpty(value))
                  parameters[property.Name.ToLowerInvariant()] = value;
          }

          var queryString = string.Join("&", parameters.Select(p => $"{p.Key}={p.Value}"));
          var hash = MD5.HashData(Encoding.UTF8.GetBytes(queryString + signingSecret));
          payload["signature"] = Convert.ToHexString(hash).ToLowerInvariant();
          return payload.ToJsonString();
      }
  }

  // Usage: create a deposit order
  var baseUrl = "https://api.c2cpayment.net";
  var payload = new JsonObject
  {
      ["orderType"] = "Deposit",
      ["amount"] = 1500,
      ["currency"] = "PKR",
      ["walletType"] = "JazzCash",
      ["merchantOrderRef"] = "INV-10045",
      ["customerReference"] = "player_12345",
      ["callbackUrl"] = "https://merchant.example.com/webhooks/c2c",
  };
  var body = C2CSigner.SignBody(payload, Environment.GetEnvironmentVariable("C2C_SIGNING_SECRET")!);

  using var http = new HttpClient();
  using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl + "/api/merchant/orders")
  {
      Content = new StringContent(body, Encoding.UTF8, "application/json"),
  };
  request.Headers.Add("C2C-API-Key", Environment.GetEnvironmentVariable("C2C_API_KEY"));
  request.Headers.Add("Idempotency-Key", "INV-10045");

  var response = await http.SendAsync(request);
  Console.WriteLine(await response.Content.ReadAsStringAsync());
  ```

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

  function signBody(payload, signingSecret) {
    const params = Object.entries(payload)
      .filter(([key, value]) =>
        key.toLowerCase() !== 'signature' &&
        value !== null && value !== undefined && value !== '' &&
        typeof value !== 'object')                                  // objects and arrays are not signed
      .map(([key, value]) => [key.toLowerCase(), String(value)])   // true → "true", 1500.5 → "1500.5"
      .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));           // ASCII order of the lowercase keys

    const queryString = params.map(([key, value]) => `${key}=${value}`).join('&');
    return crypto.createHash('md5').update(queryString + signingSecret, 'utf8').digest('hex');
  }

  // Usage: create a deposit order (Node 18+ global fetch)
  const baseUrl = 'https://api.c2cpayment.net';
  const payload = {
    orderType: 'Deposit',
    amount: 1500,
    currency: 'PKR',
    walletType: 'JazzCash',
    merchantOrderRef: 'INV-10045',
    customerReference: 'player_12345',
    callbackUrl: 'https://merchant.example.com/webhooks/c2c',
  };

  const response = await fetch(`${baseUrl}/api/merchant/orders`, {
    method: 'POST',
    headers: {
      'C2C-API-Key': process.env.C2C_API_KEY,
      'Content-Type': 'application/json',
      'Idempotency-Key': 'INV-10045',
    },
    body: JSON.stringify({ ...payload, signature: signBody(payload, process.env.C2C_SIGNING_SECRET) }),
  });
  console.log(response.status, await response.json());
  ```

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

  import requests


  def sign_body(payload: dict, signing_secret: str) -> str:
      params = {}
      for key, value in payload.items():
          if key.lower() == "signature" or value is None or value == "" or isinstance(value, (dict, list)):
              continue  # objects and arrays are not signed
          if isinstance(value, bool):
              value = "true" if value else "false"  # bool before int: True is an int in Python
          params[key.lower()] = str(value)

      query_string = "&".join(f"{key}={params[key]}" for key in sorted(params))  # ASCII order
      return hashlib.md5((query_string + signing_secret).encode("utf-8")).hexdigest()


  # Usage: create a deposit order
  base_url = "https://api.c2cpayment.net"
  payload = {
      "orderType": "Deposit",
      "amount": 1500,
      "currency": "PKR",
      "walletType": "JazzCash",
      "merchantOrderRef": "INV-10045",
      "customerReference": "player_12345",
      "callbackUrl": "https://merchant.example.com/webhooks/c2c",
  }

  response = requests.post(
      f"{base_url}/api/merchant/orders",
      json={**payload, "signature": sign_body(payload, os.environ["C2C_SIGNING_SECRET"])},
      headers={
          "C2C-API-Key": os.environ["C2C_API_KEY"],
          "Idempotency-Key": "INV-10045",
      },
  )
  print(response.status_code, response.json())
  ```
</CodeGroup>

<Tip>
  Python's `Decimal` and floats: `str(Decimal("1500.50"))` is `1500.50`, but `requests` can't serialize a `Decimal`. Send amounts as `int` or `float`, or serialize the body yourself and sign the values as they appear in it.
</Tip>

## How C2C validates a request

Order-creation requests pass through these checks in order. A request rejected at any step never reaches the next one.

```text theme={null}
Merchant request
  1. API key authentication          → 401 (empty body) if the key is missing, unknown or revoked
  2. Signature validation            → 401 SIGNATURE_* / 413 PAYLOAD_TOO_LARGE
       • body ≤ 64 KB, a single JSON object, no key repeated in a different case
       • "signature" present and a string
       • MD5 recomputed with your key's signing secret, constant-time comparison (hex case is ignored)
       • the signed body was not already accepted under a different Idempotency-Key
  3. Request validation              → 400
  4. Business rules & order creation → 200, 403, 404, 409, 422
```

C2C doesn't apply request rate limits to the Merchant API.

### Signature error responses

```json 401 Unauthorized theme={null}
{
  "success": false,
  "error": "Request signature verification failed. Sign the request as described in the signature guide.",
  "code": "SIGNATURE_INVALID",
  "traceId": "5b269362496001a7ed102e2d8cc50329"
}
```

| `code` | HTTP | Cause | Fix |
| - | - | - | - |
| `SIGNATURE_REQUIRED` | 401 | The body has no `signature` field, or it is `null` or empty | Add the `signature` field to the body |
| `SIGNATURE_MALFORMED` | 401 | The body isn't a single JSON object, `signature` isn't a string, or a key appears twice in different cases | Send one JSON object; send the signature as a string; don't repeat keys |
| `SIGNATURE_INVALID` | 401 | The signature doesn't match, or the API key has no signing secret | Check the query string, value formatting and that you use the secret belonging to this API key |
| `SIGNATURE_REPLAYED` | 401 | The same signed body was already accepted with a different `Idempotency-Key` | Retry with the original `Idempotency-Key`, or send a genuinely new order (for example a new `merchantOrderRef`). See [Replay protection](#replay-protection). |
| `PAYLOAD_TOO_LARGE` | 413 | Request body is larger than 64 KB | Reduce the body, for example a shorter `metadata` value |

C2C never returns the signature it expected, or any part of your secret. Quote the `traceId` to support if you need help investigating a rejection.

## Replay protection

A signature has no timestamp, so the same body always produces the same signature. C2C uses your `Idempotency-Key` to tell retries from replays:

| Situation | Result |
| - | - |
| You retry the same signed body with the **same** `Idempotency-Key` (for example after a timeout) | Accepted. You get the original order back; no second order is created. |
| You retry a request that was rejected (for example `422` insufficient balance, after a top-up) with the **same** `Idempotency-Key` | Accepted and processed as a new attempt. Rejected requests don't consume the key. |
| The same signed body arrives with a **different** `Idempotency-Key` within 24 hours | Rejected with `401 SIGNATURE_REPLAYED`, so a captured request can't be re-sent to create a second deposit or payout. |
| A new order | Use a new `merchantOrderRef` (and a new `Idempotency-Key`). Different fields produce a different signature. |

<Tip>
  Always send a unique `merchantOrderRef`. It is part of the signature, so it makes every order's signature unique, and C2C also refuses to reuse a `merchantOrderRef` that is already in progress or completed (`409`).
</Tip>

You don't need to re-sign a retry: the body and the `Idempotency-Key` stay exactly the same. See [Idempotency](/api-reference/idempotency).

## Common mistakes and troubleshooting

| Symptom | Likely cause | Solution |
| - | - | - |
| `SIGNATURE_INVALID` on every request | Wrong secret, a secret from another API key or environment, or a key issued without a signing secret | Use the secret issued with this key. Ask C2C to regenerate the key if it has no signing secret. |
| `SIGNATURE_INVALID` only for some amounts | The signed number differs from the number in the body, for example signing `1500.00` but sending `1500` | Sign the same object you serialize |
| `SIGNATURE_INVALID` after adding a field | A field was added to the body after signing | Add every field before signing |
| Works in one language, fails in another | Keys not lowercased, wrong sort order, booleans as `True`, or `&` before the secret | Follow the [value formatting rules](#value-formatting-rules); append the secret directly |
| `SIGNATURE_REQUIRED` | The signature was sent as a header instead of a body field | Put `signature` inside the JSON body |
| `SIGNATURE_REPLAYED` on a retry | The retry used a new `Idempotency-Key` | Reuse the original `Idempotency-Key` for retries |

### Debugging checklist

<Steps>
  <Step title="Reproduce the worked example">
    Run your signing code on the [worked examples](#worked-examples). The deposit must produce `56eff91b5ab0351471a2a8d457320230`.
  </Step>

  <Step title="Print the query string">
    Log the string you hash, with the secret removed. Keys lowercase, sorted, `key=value` joined with `&`, no `signature`, no `null` or empty values.
  </Step>

  <Step title="Check the values">
    Numbers as written in the body, booleans as `true`/`false`, strings unescaped.
  </Step>

  <Step title="Check the secret">
    The secret starts with `sksec_`, belongs to the same API key as your `C2C-API-Key` header, and is appended directly to the query string.
  </Step>

  <Step title="Check the hash">
    MD5 over UTF-8 bytes, written as 32 hex characters.
  </Step>
</Steps>

## Signing secret management

| Property | Value |
| - | - |
| Format | `sksec_` followed by 40 alphanumeric characters (46 characters in total) |
| Issued | Together with the API key, by a C2C administrator. Shown only once. |
| Scope | One secret per API key |
| Also used for | Verifying [webhooks](/guides/webhooks): C2C signs callbacks with the same secret, using HMAC-SHA256 (see the webhooks guide) |
| Rotation | C2C regenerates your API key, which issues a new key and secret and **revokes all your previously active keys** |
| Revocation | A revoked key's signing secret stops verifying immediately |

<AccordionGroup>
  <Accordion title="Keep it server-side">
    Never put the signing secret in browser JavaScript, mobile apps or anything a customer can download. Sign requests on your server only.
  </Accordion>

  <Accordion title="Store it like a password">
    Use environment variables or a secret manager such as Azure Key Vault, AWS Secrets Manager or HashiCorp Vault. Never commit it to version control, and never log it or the full string you hash.
  </Accordion>

  <Accordion title="Rotate if exposed">
    If you suspect the secret or API key has leaked, contact C2C support immediately to revoke the key and issue a new key and secret.
  </Accordion>

  <Accordion title="HTTPS only">
    Call the API over HTTPS only. The signature protects the request fields from tampering, not from being read.
  </Accordion>
</AccordionGroup>

## Migrating from header signatures

<Warning>
  **Breaking change.** C2C no longer accepts the `X-C2C-Timestamp` and `X-C2C-Signature` request headers (HMAC-SHA256 over the raw body). Order requests signed only that way are rejected with `401 SIGNATURE_REQUIRED`.
</Warning>

<Steps>
  <Step title="Replace the signing helper">
    Use one of the [code examples](#code-examples). Your signing secret doesn't change.
  </Step>

  <Step title="Move the signature into the body">
    Send `"signature": "<md5 hex>"` inside the JSON body. Stop sending `X-C2C-Timestamp` and `X-C2C-Signature`; they are ignored.
  </Step>

  <Step title="Stop signing GET requests">
    Order status and balance requests need only the API key.
  </Step>

  <Step title="Keep retries on the same Idempotency-Key">
    Retries no longer need a fresh signature: resend the same body with the same `Idempotency-Key`.
  </Step>

  <Step title="Test against the worked examples">
    Your code must reproduce the [worked examples](#worked-examples) before you deploy.
  </Step>
</Steps>

Webhooks that C2C sends you are not affected: they are still signed with HMAC-SHA256 in the `X-C2C-Signature` header.

## Next steps

<CardGroup cols={2}>
  <Card title="Create a deposit" icon="arrow-down-to-line" href="/api-reference/endpoints/create-deposit">
    Send your first signed request
  </Card>

  <Card title="Authentication" icon="key" href="/api-reference/authentication">
    API keys, required headers and auth errors
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.