Authentication & signing
Three layers of enterprise defense: network-level IP whitelisting, an API key for route authorization, and a cryptographic HMAC-SHA256 signature on every financial and state-modifying request.
Layer 1 — IP whitelisting
Billpadi enforces network-level origin IP authorization. You can configure your servers' static IPv4, IPv6, or CIDR blocks via PUT /partner/ip-whitelist or your partner portal. When configured, incoming requests originating from any IP outside your whitelist are dropped with 403 Forbidden (AUTHORIZATION_ERROR).
Layer 2 — API key authorization
Provide your live API key in the x-api-key header of every request:
| Header | Value | Applies to |
|---|---|---|
| x-api-key | sk_live_… (from POST /partner/api-keys) | All endpoints under /api/v1/partner/* |
This header authorizes read endpoints (catalogs, service providers, rate feeds, transaction queries). If missing, invalid, or belonging to an inactive partner account, the API immediately responds with 401 Unauthorized.
Layer 3 — HMAC-SHA256 request signing
Any action that creates a transaction, debits your wallet, executes a payout, or validates sensitive identity info requires two additional security headers:
| Header | Value | Description |
|---|---|---|
| x-timestamp | Unix epoch milliseconds (e.g. 1752136800000) | Prevents replay attacks within a 5-minute tolerance window |
| x-signature | Hex-encoded HMAC-SHA256 digest | Verifies payload integrity using your apiSecret |
Computing the signature
- Serialize the raw JSON body you are transmitting. For requests without a body (or empty payloads), use an empty string
"". - Construct the raw message string:
{timestamp}.{rawBody}(the timestamp string, followed by a literal period., followed by the exact raw body string). - Generate the HMAC digest using SHA-256 with your
apiSecretas the key. - Encode the digest as a lowercase hexadecimal string and transmit it as
x-signature.
Sign the exact JSON string transmitted across the wire. If your client library re-serializes or re-orders JSON keys after signing, the signature verification will fail.
const crypto = require("crypto");
function signRequest(apiSecret, timestamp, rawBody) {
const message = `${timestamp}.${rawBody}`;
return crypto.createHmac("sha256", apiSecret).update(message).digest("hex");
}
const apiKey = "sk_live_1f9c2a7e4b6d0f83a5c1e9b7d4f2038a...";
const apiSecret = "9a3e7c1b5d8f2046e1a9c7b3d5f80261...";
const payload = {
phone: "08012345678",
amount: 500,
network: "mtn",
partnerReference: "PR-9182734"
};
const rawBody = JSON.stringify(payload);
const timestamp = Date.now().toString();
const signature = signRequest(apiSecret, timestamp, rawBody);
const res = await fetch("https://api.billpadi.com/api/v1/partner/airtime/purchase", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
"x-timestamp": timestamp,
"x-signature": signature
},
body: rawBody
});
const data = await res.json();import hashlib
import hmac
import json
import time
import requests
api_key = "sk_live_1f9c2a7e4b6d0f83a5c1e9b7d4f2038a..."
api_secret = "9a3e7c1b5d8f2046e1a9c7b3d5f80261..."
payload = {
"phone": "08012345678",
"amount": 500,
"network": "mtn",
"partnerReference": "PR-9182734"
}
raw_body = json.dumps(payload, separators=(',', ':'))
timestamp = str(int(time.time() * 1000))
message = f"{timestamp}.{raw_body}"
signature = hmac.new(api_secret.encode(), message.encode(), hashlib.sha256).hexdigest()
res = requests.post(
"https://api.billpadi.com/api/v1/partner/airtime/purchase",
data=raw_body,
headers={
"Content-Type": "application/json",
"x-api-key": api_key,
"x-timestamp": timestamp,
"x-signature": signature
}
)<?php
$apiKey = "sk_live_1f9c2a7e4b6d0f83a5c1e9b7d4f2038a...";
$apiSecret = "9a3e7c1b5d8f2046e1a9c7b3d5f80261...";
$payload = [
"phone" => "08012345678",
"amount" => 500,
"network" => "mtn",
"partnerReference" => "PR-9182734"
];
$rawBody = json_encode($payload);
$timestamp = (string) round(microtime(true) * 1000);
$message = "{$timestamp}.{$rawBody}";
$signature = hash_hmac("sha256", $message, $apiSecret);
$ch = curl_init("https://api.billpadi.com/api/v1/partner/airtime/purchase");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $rawBody);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"x-api-key: {$apiKey}",
"x-timestamp: {$timestamp}",
"x-signature: {$signature}"
]);
$response = curl_exec($ch);
curl_close($ch);
?>BODY='{"phone":"08012345678","amount":500,"network":"mtn","partnerReference":"PR-9182734"}'
TIMESTAMP=$(($(date +%s%N)/1000000))
SIGNATURE=$(printf "%s.%s" "$TIMESTAMP" "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | sed "s/^.* //")
curl -X POST https://api.billpadi.com/api/v1/partner/airtime/purchase \
-H "Content-Type: application/json" \
-H "x-api-key: $API_KEY" \
-H "x-timestamp: $TIMESTAMP" \
-H "x-signature: $SIGNATURE" \
-d "$BODY"Timestamp validity window
The x-timestamp header must be within 5 minutes (300,000 ms) of server time. Requests with timestamps outside this range are rejected with 401 Request expired. Ensure your server time is continuously synchronized with standard NTP time servers.
Idempotency & partner references
Every transaction write endpoint requires a unique partnerReference string (2 to 100 characters). This ensures safe retries and prevents double charges if network interruptions occur between your servers and ours.