Some Waftpay APIs (notably Payouts and Remittance) require a request header called X-Custom-Signature.
This signature lets our platform verify who sent the request and that its key fields weren’t altered in transit.
TL;DR
- Get a Bearer token (Auth token).
- Generate an RSA 2048 key pair; register your public key with Waftpay.
- Build the signing string:
transaction.reference + transaction.amount + originator.country + transaction.service_code (no separators).
- Sign with RSA + SHA-256 (
RS256), Base64-encode the signature bytes, and send it in X-Custom-Signature.
When it’s required
- Payouts:
POST /payments-api-service/v1/payouts
- Remittance:
POST /payments-api-service/v1/remittance
These endpoints also require Bearer auth in the Authorization header.
Prerequisites
- Valid Bearer token in
Authorization: Bearer <access_token> (see Auth token).
- RSA 2048 key pair in PEM; public key registered with Waftpay (we reject unknown keys).
- Make sure the values you sign exactly match the payload you send (including casing/formatting).
Key handling and security
- Never commit private keys to your repo or docs site. Treat them as secrets.
- Store private keys in a secure vault or encrypted filesystem and load them at runtime.
- Rotate keys periodically and re-register the public key with Waftpay.
- Use different key pairs for sandbox and production.
1) Create your key pair (RSA 2048)
You need an RSA 2048 key pair in PEM format:
- Private key: PKCS#8 PEM (
-----BEGIN PRIVATE KEY-----)
- Public key: X.509 SubjectPublicKeyInfo PEM (
-----BEGIN PUBLIC KEY-----)
2) Build the signing string
Concatenate these fields in order with no separators:
transaction.reference + transaction.amount + originator.country + transaction.service_code
- Use the exact strings you send in the JSON (e.g., amount
"100.00", country "KE" vs "KEN").
- Do not trim, pad, or change case once you build the payload.
Example signing string
TXN123654573100.00KENewqeqw
Generate X-Custom-Signature
Keep your private key outside the repo (for example, ./keys/client_private.pem) and load it at runtime.
import crypto from "crypto";
import fs from "fs";
const txRef = "TXN123654573";
const amount = "100"; // stringify exactly as sent
const country = "KEN"; // match payload (KE vs KEN)
const service = "ewqeqw";
const signingString = `${txRef}${amount}${country}${service}`;
const privateKeyPem = fs.readFileSync("client_private.pem", "utf8");
const sig = crypto.sign("RSA-SHA256", Buffer.from(signingString, "utf8"), privateKeyPem);
console.log(sig.toString("base64")); // -> X-Custom-Signature
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
tx_ref = "TXN123654573"
amount = "100"
country = "KEN"
service = "ewqeqw"
signing_string = f"{tx_ref}{amount}{country}{service}".encode("utf-8")
with open("client_private.pem", "rb") as f:
key = serialization.load_pem_private_key(f.read(), password=None)
sig = key.sign(signing_string, padding.PKCS1v15(), hashes.SHA256())
print(base64.b64encode(sig).decode("utf-8")) # -> X-Custom-Signature
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public final class WaftpaySignature {
public static String generateXCustomSignature(
String txRef,
String amount,
String country,
String service,
PrivateKey privateKey
) throws Exception {
String signingString = txRef + amount + country + service;
byte[] message = signingString.getBytes(StandardCharsets.UTF_8);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update(message);
byte[] raw = sig.sign();
return Base64.getEncoder().encodeToString(raw);
}
public static PrivateKey loadPkcs8PrivateKey(Path pemPath) throws Exception {
String pem = Files.readString(pemPath, StandardCharsets.US_ASCII);
String base64 = pem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] der = Base64.getDecoder().decode(base64);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(der);
return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
}
public static void main(String[] args) throws Exception {
PrivateKey key = loadPkcs8PrivateKey(Path.of("client_private.pem"));
String txRef = "TXN123654573";
String amount = "100";
String country = "KEN";
String service = "ewqeqw";
String xCustomSignature = generateXCustomSignature(txRef, amount, country, service, key);
System.out.println("X-Custom-Signature: " + xCustomSignature);
}
}
<?php
$signingString = "TXN123654573100KENewqeqw";
$privateKey = openssl_pkey_get_private(file_get_contents("client_private.pem"));
openssl_sign($signingString, $rawSig, $privateKey, OPENSSL_ALGO_SHA256);
echo base64_encode($rawSig), PHP_EOL; // -> X-Custom-Signature
Use standard Base64 (not Base64URL). Algorithm is RS256 (RSA PKCS#1 v1.5 + SHA-256).
Common mistakes
- Mismatched amount formatting: if you send
"100.00" you must sign "100.00" (not "100").
- Country/service casing:
KE vs KEN or service code casing must match exactly.
- Wrong key format: use PKCS#8 (
BEGIN PRIVATE KEY), not PKCS#1 (BEGIN RSA PRIVATE KEY).
- Base64URL vs Base64: use standard Base64 (
+ and /), not URL-safe Base64.
- Payload mismatch: any difference between the signed string and the sent JSON will fail verification.
OpenSSL (Linux/macOS/Windows with OpenSSL)
# 1) Generate a 2048-bit RSA private key (PKCS#8)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out client_private.pem
# 2) Derive the matching public key (X.509 SPKI)
openssl rsa -pubout -in client_private.pem -out client_public.pem