Appearance
API Key Authentication
Every API request to IIMMPACT must be authenticated using your API key and signed with an HMAC-SHA256 signature. This page walks you through the entire process.
Prerequisites
You need two credentials to authenticate:
- API Key — your unique identifier (e.g.,
iimm_prod_abc123...) - HMAC Secret — a Base64-encoded secret used to sign each request
To generate these, log in to the IIMMPACT Dashboard and navigate to Developer > API Keys. Click Generate Key and store both values immediately.
DANGER
Both the API key and HMAC secret are displayed only once. If you lose them, you must rotate the key to generate new credentials.
How It Works
Every request you send must include four headers. The server uses these to verify that:
- The request came from you (API key)
- The request hasn't been tampered with (signature)
- The request is recent (timestamp)
- The request isn't a replay (nonce)
| Header | What to set it to |
|---|---|
X-Api-Key | Your API key |
X-Timestamp | Current Unix timestamp in seconds |
X-Nonce | A unique random string (16-128 chars, e.g., UUID) |
X-Signature | v1= followed by the computed signature |
The rest of this page shows you how to compute the signature.
Topup Endpoint Restriction
All endpoints use API Key + HMAC authentication, and JWT authentication is deprecated for new integrations.
Account owners can enforce API Key-only authentication for topup requests via the dashboard to block any remaining legacy JWT traffic.
When enabled:
- API Key requests — continue to work as normal
- Legacy JWT requests to the topup endpoint — return
403 Forbidden
How to Configure
Navigate to Developer > API Keys > Security Settings in the IIMMPACT Dashboard and toggle Require API Key for Topup.
WARNING
Enabling this setting immediately blocks all JWT-authenticated topup requests, including those from the dashboard itself. Ensure all clients use API Key + HMAC authentication before enabling.
Error Response
Legacy JWT requests to the topup endpoint when this setting is enabled will receive:
- HTTP 403 Forbidden
- Message:
"This reseller requires API key authentication for topup. JWT authentication is not allowed."
TIP
Changes may take up to 5 minutes to take effect due to caching.
Signing Step by Step
Step 1: Generate timestamp and nonce
bash
TIMESTAMP=$(date +%s) # e.g., 1706500000
NONCE="req-${TIMESTAMP}-$(openssl rand -hex 8)" # e.g., req-1706500000-a1b2c3d4e5f6g7h8The timestamp must be within 5 minutes of the server's time. The nonce must be unique per request — a UUID or timestamp + random hex works well.
TIP
Nonce requirements: 16-128 characters, only a-z A-Z 0-9 - _. Reused nonces are rejected for 10 minutes.
Step 2: Hash the request body
Compute the SHA-256 hash of your request body, then Base64-encode it.
bash
# For POST/PUT with a body:
BODY='{"account":"1234567890","product":"TNB","amount":100.00}'
BODY_HASH=$(echo -n "${BODY}" | openssl dgst -sha256 -binary | base64)
# Result: KYo/5gXXNzwWa9nyFJJMMwwZYiZgDfFKGNkU0+E3rmY=
# For GET/DELETE (no body), hash an empty string:
BODY_HASH=$(echo -n "" | openssl dgst -sha256 -binary | base64)
# Result: 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=Step 3: Build the canonical string
Join these values with colons, in this exact order:
v1:{timestamp}:{nonce}:{METHOD}:{query}:{bodyHash}Example — for a GET request to /v2/bill-presentment?account=1234567890&product=TNB:
bash
CANONICAL="v1:1706500000:req-1706500000-a1b2c3d4e5f6g7h8:GET:account=1234567890&product=TNB:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="Key rules:
METHODmust be uppercase (GET, notget)queryis everything after?but without the?itself. Empty string if there is no query string- Query parameters must be sorted alphabetically by key before building the canonical string. For example,
product=TNB&account=123must be sorted toaccount=123&product=TNB. Parameters without a=(e.g., bare flags) should be excluded
Step 4: Compute the signature
Sign the canonical string with your HMAC secret. The secret must be Base64-decoded before use. Convert to hex first to safely handle binary keys:
bash
HEXKEY=$(echo -n "${HMAC_SECRET}" | base64 -d | xxd -p -c 256)
SIGNATURE=$(echo -n "${CANONICAL}" \
| openssl dgst -sha256 -mac HMAC -macopt "hexkey:${HEXKEY}" \
-binary \
| base64)Prefix the result with v1=:
bash
X-Signature: v1=${SIGNATURE}Complete Examples
GET Request
bash
#!/bin/bash
# Your credentials
API_KEY="iimm_prod_your_api_key_here"
HMAC_SECRET="your_base64_hmac_secret_here"
# Request details
METHOD="GET"
URL_PATH="/v2/bill-presentment"
QUERY="account=1234567890&product=TNB"
# Step 1: Timestamp and nonce
TIMESTAMP=$(date +%s)
NONCE="req-${TIMESTAMP}-$(openssl rand -hex 8)"
# Step 2: Body hash (empty for GET)
BODY_HASH=$(echo -n "" | openssl dgst -sha256 -binary | base64)
# Step 3: Sort query params alphabetically by key, then build canonical string
SORTED_QUERY=$(echo "${QUERY}" | tr '&' '\n' | sort | tr '\n' '&' | sed 's/&$//')
CANONICAL="v1:${TIMESTAMP}:${NONCE}:${METHOD}:${SORTED_QUERY}:${BODY_HASH}"
# Step 4: Signature
HEXKEY=$(echo -n "${HMAC_SECRET}" | base64 -d | xxd -p -c 256)
SIGNATURE=$(echo -n "${CANONICAL}" \
| openssl dgst -sha256 -mac HMAC -macopt "hexkey:${HEXKEY}" \
-binary \
| base64)
# Send the request
curl -X GET "https://api.iimmpact.com${URL_PATH}?${QUERY}" \
-H "X-Api-Key: ${API_KEY}" \
-H "X-Timestamp: ${TIMESTAMP}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Signature: v1=${SIGNATURE}" \
-H "Content-Type: application/json"POST Request
bash
#!/bin/bash
# Your credentials
API_KEY="iimm_prod_your_api_key_here"
HMAC_SECRET="your_base64_hmac_secret_here"
# Request details
METHOD="POST"
URL_PATH="/v2/topup"
QUERY=""
BODY='{"refid":"unique-ref-123","account":"1234567890","product":"TNB","amount":100.00}'
# Step 1: Timestamp and nonce
TIMESTAMP=$(date +%s)
NONCE="req-${TIMESTAMP}-$(openssl rand -hex 8)"
# Step 2: Body hash (hash the actual body for POST)
BODY_HASH=$(echo -n "${BODY}" | openssl dgst -sha256 -binary | base64)
# Step 3: Canonical string
CANONICAL="v1:${TIMESTAMP}:${NONCE}:${METHOD}:${QUERY}:${BODY_HASH}"
# Step 4: Signature
HEXKEY=$(echo -n "${HMAC_SECRET}" | base64 -d | xxd -p -c 256)
SIGNATURE=$(echo -n "${CANONICAL}" \
| openssl dgst -sha256 -mac HMAC -macopt "hexkey:${HEXKEY}" \
-binary \
| base64)
# Send the request
curl -X POST "https://api.iimmpact.com${URL_PATH}" \
-H "X-Api-Key: ${API_KEY}" \
-H "X-Timestamp: ${TIMESTAMP}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Signature: v1=${SIGNATURE}" \
-H "Content-Type: application/json" \
-d "${BODY}"Code Examples
Signed GET /v2/balance
bash
#!/bin/bash
# Your credentials
API_KEY="iimm_prod_your_api_key_here"
HMAC_SECRET="your_base64_hmac_secret_here"
# Request details
METHOD="GET"
URL_PATH="/v2/balance"
QUERY="" # No query params for this request
# Step 1: Timestamp and nonce
TIMESTAMP=$(date +%s)
NONCE="req-${TIMESTAMP}-$(openssl rand -hex 8)"
# Step 2: Body hash (GET/DELETE always hashes empty string)
BODY_HASH=$(echo -n "" | openssl dgst -sha256 -binary | base64)
# Step 3: Canonical string: v1:{timestamp}:{nonce}:{METHOD}:{sortedQuery}:{bodyHash}
CANONICAL="v1:${TIMESTAMP}:${NONCE}:${METHOD}:${QUERY}:${BODY_HASH}"
# Step 4: Base64-decode HMAC secret, sign canonical string, then prefix with v1=
HEXKEY=$(echo -n "${HMAC_SECRET}" | base64 -d | xxd -p -c 256)
SIGNATURE=$(echo -n "${CANONICAL}" \
| openssl dgst -sha256 -mac HMAC -macopt "hexkey:${HEXKEY}" \
-binary \
| base64)
echo "Headers being sent:"
echo "X-Api-Key: ${API_KEY}"
echo "X-Timestamp: ${TIMESTAMP}"
echo "X-Nonce: ${NONCE}"
echo "X-Signature: v1=${SIGNATURE}"
curl -X GET "https://api.iimmpact.com${URL_PATH}" \
-H "X-Api-Key: ${API_KEY}" \
-H "X-Timestamp: ${TIMESTAMP}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Signature: v1=${SIGNATURE}" \
-H "Content-Type: application/json"javascript
// Run with Node.js 18+ (fetch built-in): node balance-signature.mjs
import crypto from "node:crypto";
const API_KEY = "iimm_prod_your_api_key_here";
const HMAC_SECRET = "your_base64_hmac_secret_here"; // Base64 from dashboard
const BASE_URL = "https://api.iimmpact.com";
const METHOD = "GET";
const URL_PATH = "/v2/balance";
// For /v2/balance there are no query params.
// Example with params: { product: "D", status: "Active" }
const queryParams = {};
const sortedQuery = new URLSearchParams(
Object.entries(queryParams)
.filter(([, value]) => value !== undefined && value !== null && value !== "")
.sort(([a], [b]) => a.localeCompare(b)),
).toString(); // No leading "?"
// Step 1: Timestamp and nonce
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = `req-${timestamp}-${crypto.randomBytes(8).toString("hex")}`;
// Step 2: Body hash (GET/DELETE always hashes empty string)
const bodyHash = crypto.createHash("sha256").update("").digest("base64");
// 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
// Step 3: Canonical string: v1:{timestamp}:{nonce}:{METHOD}:{sortedQuery}:{bodyHash}
const canonical = `v1:${timestamp}:${nonce}:${METHOD}:${sortedQuery}:${bodyHash}`;
// Step 4: Base64-decode HMAC secret, sign canonical string, then prefix with v1=
const hmacKey = Buffer.from(HMAC_SECRET, "base64");
const signature = `v1=${crypto.createHmac("sha256", hmacKey).update(canonical).digest("base64")}`;
const url = sortedQuery ? `${BASE_URL}${URL_PATH}?${sortedQuery}` : `${BASE_URL}${URL_PATH}`;
const headers = {
"X-Api-Key": API_KEY,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
"Content-Type": "application/json",
};
console.log("Canonical string:", canonical);
console.log("Headers being sent:", headers);
const response = await fetch(url, {
method: METHOD,
headers,
});
const responseText = await response.text();
console.log("Status:", response.status);
console.log("Body:", responseText);python
# pip install requests
import base64
import hashlib
import hmac
import time
import uuid
import requests
API_KEY = "iimm_prod_your_api_key_here"
HMAC_SECRET = "your_base64_hmac_secret_here" # Base64 from dashboard
BASE_URL = "https://api.iimmpact.com"
METHOD = "GET"
URL_PATH = "/v2/balance"
# For /v2/balance there are no query params.
# Example with query params: {"product": "D", "status": "Active"}
query_params = {}
sorted_query = "&".join(
f"{key}={value}"
for key, value in sorted(query_params.items(), key=lambda item: item[0])
if value is not None and value != ""
) # No leading "?"
# Step 1: Timestamp and nonce
timestamp = str(int(time.time()))
nonce = f"req-{timestamp}-{uuid.uuid4().hex[:16]}"
# Step 2: Body hash (GET/DELETE always hashes empty string)
body_hash = base64.b64encode(hashlib.sha256(b"").digest()).decode("ascii")
# 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
# Step 3: Canonical string: v1:{timestamp}:{nonce}:{METHOD}:{sortedQuery}:{bodyHash}
canonical = f"v1:{timestamp}:{nonce}:{METHOD}:{sorted_query}:{body_hash}"
# Step 4: Base64-decode HMAC secret, sign canonical string, then prefix with v1=
hmac_key = base64.b64decode(HMAC_SECRET)
signature_raw = hmac.new(hmac_key, canonical.encode("utf-8"), hashlib.sha256).digest()
signature = "v1=" + base64.b64encode(signature_raw).decode("ascii")
url = f"{BASE_URL}{URL_PATH}"
if sorted_query:
url = f"{url}?{sorted_query}"
headers = {
"X-Api-Key": API_KEY,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
"Content-Type": "application/json",
}
print("Canonical string:", canonical)
print("Headers being sent:", headers)
response = requests.get(url, headers=headers, timeout=30)
print("Status:", response.status_code)
print("Body:", response.text)Troubleshooting
Getting 401 Unauthorized? Check these common mistakes:
| Problem | What to check |
|---|---|
| Signature mismatch | Make sure you Base64-decode the HMAC secret before signing. The secret is stored as Base64 — you must decode it first. |
| Wrong canonical string | Print your canonical string and verify the order: v1:{timestamp}:{nonce}:{METHOD}:{query}:{bodyHash}. Every component matters. |
Query string includes ? | The query component must not include the leading ?. Use account=123&product=TNB, not ?account=123&product=TNB. |
| Lowercase HTTP method | The method must be uppercase: GET, POST, DELETE — not get, post, delete. |
| Wrong body hash | For GET/DELETE requests, hash an empty string (""), not null or nothing. For POST/PUT, hash the exact body you send. |
| Timestamp expired | Your server clock may be out of sync. The timestamp must be within 5 minutes of the server's time. Use NTP to keep your clock accurate. |
| Nonce reused | Each request must have a unique nonce. If you retry a request, generate a new nonce. |
Reference
Validation Rules
| Field | Rule |
|---|---|
| Timestamp | Valid Unix timestamp in seconds, within ±5 minutes of server time |
| Nonce | 16-128 characters, alphanumeric plus - and _, unique per request (10-minute replay window) |
| Signature | Must start with v1=, valid Base64-encoded HMAC-SHA256 |
| Request body | Maximum 10 MB |
Error Responses
Authentication failures return a JSON body with a machine-readable error code and a human-readable message:
json
{
"error": "timestamp_expired",
"message": "X-Timestamp is outside the ±5 minute tolerance window"
}401 Unauthorized
| Error Code | Cause |
|---|---|
missing_api_key | X-Api-Key header is absent or empty |
invalid_api_key | API key not found or revoked |
hmac_not_configured | API key exists but has no HMAC secret (contact support) |
missing_hmac_headers | X-Timestamp, X-Nonce, or X-Signature is missing |
empty_hmac_values | One or more HMAC headers are present but empty |
invalid_nonce_format | Nonce does not meet the length or character requirements |
invalid_timestamp_format | Not a valid Unix timestamp |
timestamp_expired | Outside the 5-minute tolerance window |
invalid_signature_format | Signature does not start with v1= |
signature_too_large | Signature exceeds maximum size |
body_too_large | Request body exceeds 10 MB limit |
invalid_signature | HMAC verification failed |
nonce_reused | This nonce was already used within the last 10 minutes |
decryption_error | HMAC secret could not be decrypted (contact support) |
internal_error | Unexpected error during authentication |
503 Service Unavailable
The nonce validation service is temporarily unavailable. Retry with exponential backoff.
