Skip to content

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:

  1. API Key — your unique identifier (e.g., iimm_prod_abc123...)
  2. 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)
HeaderWhat to set it to
X-Api-KeyYour API key
X-TimestampCurrent Unix timestamp in seconds
X-NonceA unique random string (16-128 chars, e.g., UUID)
X-Signaturev1= 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-a1b2c3d4e5f6g7h8

The 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:

  • METHOD must be uppercase (GET, not get)
  • query is 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=123 must be sorted to account=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:

ProblemWhat to check
Signature mismatchMake sure you Base64-decode the HMAC secret before signing. The secret is stored as Base64 — you must decode it first.
Wrong canonical stringPrint 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 methodThe method must be uppercase: GET, POST, DELETE — not get, post, delete.
Wrong body hashFor GET/DELETE requests, hash an empty string (""), not null or nothing. For POST/PUT, hash the exact body you send.
Timestamp expiredYour 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 reusedEach request must have a unique nonce. If you retry a request, generate a new nonce.

Reference

Validation Rules

FieldRule
TimestampValid Unix timestamp in seconds, within ±5 minutes of server time
Nonce16-128 characters, alphanumeric plus - and _, unique per request (10-minute replay window)
SignatureMust start with v1=, valid Base64-encoded HMAC-SHA256
Request bodyMaximum 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 CodeCause
missing_api_keyX-Api-Key header is absent or empty
invalid_api_keyAPI key not found or revoked
hmac_not_configuredAPI key exists but has no HMAC secret (contact support)
missing_hmac_headersX-Timestamp, X-Nonce, or X-Signature is missing
empty_hmac_valuesOne or more HMAC headers are present but empty
invalid_nonce_formatNonce does not meet the length or character requirements
invalid_timestamp_formatNot a valid Unix timestamp
timestamp_expiredOutside the 5-minute tolerance window
invalid_signature_formatSignature does not start with v1=
signature_too_largeSignature exceeds maximum size
body_too_largeRequest body exceeds 10 MB limit
invalid_signatureHMAC verification failed
nonce_reusedThis nonce was already used within the last 10 minutes
decryption_errorHMAC secret could not be decrypted (contact support)
internal_errorUnexpected error during authentication

503 Service Unavailable

The nonce validation service is temporarily unavailable. Retry with exponential backoff.

IIMMPACT API Documentation