API Encryption
Some merchant accounts are enrolled in API payload encryption. When encryption is enabled for your business, the Merchant API decrypts non-GET request bodies and encrypts successful HTTP 200 responses.
Current Server Behaviour
The implementation uses Laravel’s encrypter with AES-256-GCM. The encryption key must be exactly 32 bytes. For enrolled businesses:
POSTandPUTrequest bodies must be encrypted before they reach the controller.GETrequest bodies are not decrypted.- Successful HTTP
200responses are encrypted and returned asapplication/octet-stream. - The server does not rely on
X-Encrypted-PayloadorX-Encrypt-Responseheaders to decide whether encryption applies; enrollment controls the behaviour.
If the encrypted body cannot be decrypted, the API returns:
{
"error": "Failed to decrypt payload."
}If the encryption key is missing or invalid, the API returns:
{
"error": "Invalid or missing encryption key. Kindly reach out to our customer care"
}Encrypted Payload Format
Send the Laravel-compatible encrypted string as the raw request body. After base64 decoding, the payload has this JSON shape:
{
"iv": "base64-iv",
"value": "base64-ciphertext",
"mac": "",
"tag": "base64-auth-tag"
}The full JSON object is then base64 encoded and sent as the HTTP body.
Example: Plain Payout Body Before Encryption
{
"account_number": "0123456789",
"account_name": "John Doe",
"bank_code": "000013",
"amount": "100",
"currency": "NGN",
"sender_name": "Jane Smith",
"narration": "Payment for services",
"reference": "ngn_ref_123"
}Example Request
curl -X POST "https://api.cashonrails.com/api/v1/bank_transfer" \
-H "Authorization: Bearer <secret_key>" \
-H "Content-Type: application/octet-stream" \
-H "X-Signature: <base64-rsa-signature>" \
-H "Idempotency-Key: ngn_ref_123" \
--data-binary '<laravel-compatible-encrypted-payload>'For payout requests, sign the exact encrypted request body if encryption is enabled for your account, because the server verifies X-Signature against the raw body it receives.
Node.js Encryption Example
const crypto = require('crypto');
function encryptForCashonrails(payload, encryptionKey) {
const key = Buffer.from(encryptionKey, 'utf8');
if (key.length !== 32) throw new Error('Encryption key must be 32 bytes');
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const value = Buffer.concat([
cipher.update(JSON.stringify(payload), 'utf8'),
cipher.final()
]).toString('base64');
const envelope = {
iv: iv.toString('base64'),
value,
mac: '',
tag: cipher.getAuthTag().toString('base64')
};
return Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64');
}Decrypting a Response
Treat an encrypted 200 response body as the same base64-encoded envelope and decrypt it with the same 32-byte encryption key. The decrypted value is the original JSON response string.
Common Pitfalls
- Sending normal JSON after API encryption has been enabled for the business.
- Using a key shorter or longer than 32 bytes.
- Signing a different string than the body sent to the API.
- Parsing encrypted
200responses as JSON before decrypting them. - Assuming encryption replaces HTTPS, bearer auth, webhook verification, or RSA signing. It does not.