Encryption¶
SecPaid optionally encrypts webhook payloads sent to your payment_endpoint. When enabled, the JSON payload is encrypted with AES-256-CBC before delivery.
Enabling Encryption¶
Encryption is controlled by two SecPaid account attributes on your account:
| Attribute | Value | Description |
|---|---|---|
is_encryption |
Yes / No |
Enable or disable payload encryption |
EncryptionKey |
32-char string | Your AES-256 encryption key |
Contact SecPaid support to enable encryption and set your key, or use the updateSecPaidAttribute API.
Auto-generated key
If you set attributeName: "EncryptionKey" without a value via the API, SecPaid auto-generates a secure key for you.
How It Works¶
When encryption is enabled, the webhook delivery changes:
Without encryption:
{
"ResponseCode": 1,
"data": {
"pay_id": 12345,
"note": "Invoice #1234",
"amount": 49.99,
"user_id": "abc-123",
"status": "Success"
}
}
With encryption:
Encryption Process¶
SecPaid performs these steps:
- Serialize the full payload object to JSON
- Encrypt using AES-256-CBC:
- Key: Your 32-character
EncryptionKey - IV (Initialization Vector): First 16 characters of your
EncryptionKey
- Key: Your 32-character
- Base64-encode the encrypted bytes
- Send as
{"data": "<base64_string>"}
Decryption¶
PHP¶
function decryptSecPaidWebhook(string $encryptedData, string $encryptionKey): array
{
$decrypted = openssl_decrypt(
$encryptedData,
'AES-256-CBC',
$encryptionKey,
0,
substr($encryptionKey, 0, 16) // IV = first 16 chars of key
);
return json_decode($decrypted, true);
}
// Usage in webhook handler:
$payload = $request->all();
if (isset($payload['data']) && is_string($payload['data'])) {
$payload = decryptSecPaidWebhook(
$payload['data'],
env('SECPAID_ENCRYPTION_KEY')
);
}
// $payload now contains the standard structure:
// ['ResponseCode' => 1, 'data' => ['pay_id' => ..., ...]]
JavaScript (Node.js)¶
const crypto = require('crypto');
function decryptSecPaidWebhook(encryptedData, encryptionKey) {
const iv = encryptionKey.substring(0, 16);
const decipher = crypto.createDecipheriv(
'aes-256-cbc',
encryptionKey,
iv
);
let decrypted = decipher.update(encryptedData, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
// Usage:
app.post('/webhook', (req, res) => {
let payload = req.body;
if (typeof payload.data === 'string') {
payload = decryptSecPaidWebhook(
payload.data,
process.env.SECPAID_ENCRYPTION_KEY
);
}
// payload.data.pay_id, payload.data.amount, etc.
});
Python¶
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import base64
import json
def decrypt_secpaid_webhook(encrypted_data: str, encryption_key: str) -> dict:
key = encryption_key.encode('utf-8')
iv = key[:16]
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = unpad(
cipher.decrypt(base64.b64decode(encrypted_data)),
AES.block_size
)
return json.loads(decrypted.decode('utf-8'))
Security Considerations¶
- Store your
EncryptionKeysecurely (environment variables, secrets manager) - The IV is derived from the key (first 16 chars) — this is a known trade-off for simplicity
- Rotate your key periodically by updating the
EncryptionKeyattribute - Always validate the decrypted payload structure before processing
- If decryption fails, log the error and investigate — do not process the raw encrypted data
Testing¶
In the development environment, encryption works identically to production. Test your decryption logic with test payments before going live.