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 were not 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 with no separators.
- Sign with RSA + SHA-256, Base64-encode the signature bytes, and send it in
X-Custom-Signature.
When it’s required
- Payouts:
POST /payments/api/v1/payouts
- Remittance:
POST /payments/api/v1/remittance
These endpoints also require Bearer auth in the Authorization header.
Environments
| Environment | Base URL |
|---|
| Sandbox | https://dev.waftpay.io |
| Production | https://waftpay.io |
Use separate key pairs for Sandbox and Production. Register the correct public key for each environment.
Prerequisites
- Valid Bearer token in
Authorization: Bearer <access_token> (see Auth token).
- RSA 2048 key pair in PEM; public key registered with Waftpay.
- Make sure the values you sign exactly match the payload you send, including casing and 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, for example amount
"100.00" and country "KE" vs "KEN".
- Do not trim, pad, round, 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.00"; // stringify exactly as sent
const country = "KEN"; // match payload exactly
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.00"
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.00";
String country = "KEN";
String service = "ewqeqw";
String xCustomSignature = generateXCustomSignature(
txRef,
amount,
country,
service,
key
);
System.out.println("X-Custom-Signature: " + xCustomSignature);
}
}
<?php
$txRef = "TXN123654573";
$amount = "100.00";
$country = "KEN";
$service = "ewqeqw";
$signingString = $txRef . $amount . $country . $service;
$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. The signing algorithm is RSA PKCS#1 v1.5 + SHA-256.
Authorization: Bearer <access_token>
X-Custom-Signature: <base64_signature>
Content-Type: application/json
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 with
+ 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