Skip to content

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:

{
  "data": "U2FsdGVkX1+abc123...base64_encrypted_payload..."
}

Encryption Process

SecPaid performs these steps:

  1. Serialize the full payload object to JSON
  2. Encrypt using AES-256-CBC:
    • Key: Your 32-character EncryptionKey
    • IV (Initialization Vector): First 16 characters of your EncryptionKey
  3. Base64-encode the encrypted bytes
  4. 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 EncryptionKey securely (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 EncryptionKey attribute
  • 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.