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 were not 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 with no separators.
  4. 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

EnvironmentBase URL
Sandboxhttps://dev.waftpay.io
Productionhttps://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
Use standard Base64, not Base64URL. The signing algorithm is RSA PKCS#1 v1.5 + SHA-256.

Example request headers

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