Skip to main content
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
  1. Get a Bearer token (Auth token).
  2. Generate an RSA 2048 key pair; register your public key with Waftpay.
  3. Build the signing string: transaction.reference + transaction.amount + originator.country + transaction.service_code (no separators).
  4. 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
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