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

# Request Signing

> Most POST endpoints require a request signature so Numero can verify the integrity and authenticity of the payload.

Most endpoints require a cryptographic signature so Numero can verify the integrity and authenticity of the request. The signature is HMAC-SHA256 and travels in the `X-Numero-Signature` header.

This is **not** limited to POST. Several `GET` endpoints that return tenant data — your balance, transfer status, transaction list, virtual accounts, cards, cardholders, customers and invoices — are signed too, so that a leaked read-only API key cannot read your account without the signing secret. Every page in the API reference states **Signature required: Yes/No** for its endpoint; that field is generated from the source code, so trust it over any example you find elsewhere.

<Warning>
  **Two different keys.** The `X-Numero-Api-Key` header identifies you. The **Public Key** is the HMAC secret you sign with. They are different values, and despite its name the Public Key is a *secret* — never ship it in client-side code or publish it. Signing with the API key will never produce a valid signature.
</Warning>

## How it works

1. Take the **exact JSON body you will send**, byte for byte (camelCase property names)
2. Compute an HMAC-SHA256 hash of it, using the **UTF-8 bytes of your Public Key** as the secret
3. Base64-encode the resulting hash
4. Send it in the `X-Numero-Signature` header, along with `X-Numero-Signature-Version: v2`

<Warning>
  Your Public Key is used as the HMAC secret **as-is — the raw UTF-8 bytes of the key string**. Do **not** Base64-decode it first: that produces a different secret and your signature will never validate.
</Warning>

<Note>
  **Sign exactly what you send.** The signature is verified against the raw body bytes on the wire. If you serialize the object twice — once to sign and once to send — and the two differ by even a space or key order, verification fails. Build the JSON string once, sign that string, and send that same string.
</Note>

## Code examples

### Node.js

```javascript theme={null}
const crypto = require("crypto");

function sign(rawBody, publicKey) {
  // key = UTF-8 bytes of the public key, NOT base64-decoded
  const hmac = crypto.createHmac("sha256", Buffer.from(publicKey, "utf8"));
  hmac.update(rawBody, "utf8");
  return hmac.digest("base64");
}

// Build the body ONCE, sign it, then send that exact string.
const rawBody = JSON.stringify({
  narration: "Payment for order #123",
  amount: 5000,
  destinationAccountNumber: "0123456789",
  destinationBankCode: "000013",
  destinationAccountName: "John Doe",
  phoneNumber: "08012345678"
});

const signature = sign(rawBody, "your_public_key_here");
// fetch(url, { method: "POST", body: rawBody, headers: { ...signature headers } })
```

### Python

```python theme={null}
import hmac
import hashlib
import base64
import json

def sign(raw_body: str, public_key: str) -> str:
    # key = UTF-8 bytes of the public key, NOT base64.b64decode(public_key)
    digest = hmac.new(public_key.encode("utf-8"), raw_body.encode("utf-8"), hashlib.sha256).digest()
    return base64.b64encode(digest).decode("utf-8")

raw_body = json.dumps(
    {"narration": "Payment for order #123", "amount": 5000},
    separators=(",", ":"),
)
signature = sign(raw_body, "your_public_key_here")
# requests.post(url, data=raw_body, headers={...})
```

### C\#

```csharp theme={null}
using System.Security.Cryptography;
using System.Text;

public static string Sign(string rawBody, string publicKey)
{
    // key = UTF-8 bytes of the public key, NOT Convert.FromBase64String(publicKey)
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(publicKey));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
    return Convert.ToBase64String(hash);
}
```

### PHP

```php theme={null}
function sign($rawBody, $publicKey) {
    // key = the public key string as-is, NOT base64_decode($publicKey)
    return base64_encode(hash_hmac('sha256', $rawBody, $publicKey, true));
}
```

## Full request example

```bash theme={null}
curl -X POST "https://api.usenumero.com/numeroaccount/api/v1/business/single" \
  -H "Content-Type: application/json" \
  -H "X-Numero-Api-Key: your_api_key_here" \
  -H "X-Numero-Signature: 4KkwgCUsJ5mDSYsdpK4YKHEfXXboTw0yw4rnV22ReX8=" \
  -H "X-Numero-Signature-Version: v2" \
  -d '{
    "narration": "Payment for order #123",
    "amount": 5000,
    "destinationAccountNumber": "0123456789",
    "destinationBankCode": "000013",
    "destinationAccountName": "John Doe",
    "phoneNumber": "08012345678"
  }'
```

## Signing GET requests

Signed `GET` endpoints have no body, so the input is the **canonicalized query string**:

1. URL-decode each value once.
2. Sort parameters by key, case-sensitive (ordinal).
3. If a key appears more than once, sort its values ordinally too.
4. Join every pair as `key=value`, separated by `&`.
5. If there is no query string at all, sign the **empty string**.

So `GET /api/v1/business/balance?currency=NGN` signs the exact string `currency=NGN` — not the full URL, and not the path.

## Check your implementation offline

You do not need to call Numero to know whether your signing code is right. These vectors are computed with the same HMAC the server uses, so if you reproduce all three you are correct.

Using the key `pk_test_numero_example_key`:

| Input          | What you sign                                                                                               | Expected `X-Numero-Signature`                  |
| -------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| POST body      | `{"amount":5000,"destinationAccountNumber":"0123456789","destinationBankCode":"000013","narration":"Test"}` | `WogW6SBhpndDyZ9btTV6ZDZqO47g9GSviPDgN4HNj28=` |
| GET with query | `currency=NGN`                                                                                              | `HMhGDicrmmPc6EcQ/LQt+bWbkd8bWHfHjj+6eXPAI98=` |
| GET, no query  | *(empty string)*                                                                                            | `9Zt3NI0SO2I8i2Cg03rZ/7z5cq1YCX32P/I/j+ophxI=` |

Reproduce them from your shell:

```bash theme={null}
printf '%s' 'currency=NGN' \
  | openssl dgst -sha256 -hmac 'pk_test_numero_example_key' -binary \
  | base64
# HMhGDicrmmPc6EcQ/LQt+bWbkd8bWHfHjj+6eXPAI98=
```

<Warning>
  There is also a `POST /api/v1/business/generatesignature?publickey=...` helper. Avoid it. It takes your **signing secret in the query string**, where it is routinely written to server access logs, proxy and CDN logs, browser history and monitoring tools. Use the offline vectors above instead. If you have already used the helper with a live key, rotate that key.
</Warning>

## Legacy headers

Older integrations send the API key as `x-api-key` and the signature as `x-signature-key`. Those **still work** and will keep being accepted, but new integrations should use the canonical `X-Numero-Api-Key` / `X-Numero-Signature` headers shown above.

## Important notes

* The Public Key is the HMAC secret, used as its **raw UTF-8 bytes** — never Base64-decode it
* Property names in the JSON body **must be camelCase**
* Sign the **exact bytes you send** — no re-serialization between signing and sending
* Ensure your JSON serialization produces consistent output (no extra whitespace, consistent key ordering)
