X-CreptaPay-Signature header in every webhook request. The signature is generated using a SHA-256 HMAC of the event’s data payload with your merchant Secret API Key (sk_...).
Verification steps
- Retrieve the
X-CreptaPay-Signatureheader from the incoming request. - Extract the
dataobject from the request body. - Generate a SHA-256 HMAC of the stringified
dataobject using your secret API key as the key. - Compare your generated signature with the signature retrieved from the header.
You must perform a constant-time comparison to prevent timing attacks.
Code examples
Here is how to verify webhook signatures in different languages:Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
const signature = req.headers['x-creptapay-signature'];
const secretKey = 'sk_live_...'; // Your secret key
if (!signature) {
return res.status(400).send('Missing signature');
}
// Retrieve the inner data object
const dataPayload = req.body.data;
// Calculate signature
const calculatedSignature = crypto
.createHmac('sha256', secretKey)
.update(JSON.stringify(dataPayload))
.digest('hex');
// Perform constant-time comparison
const isValid = crypto.timingSafeEqual(
Buffer.from(signature, 'utf-8'),
Buffer.from(calculatedSignature, 'utf-8')
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Handle the verified event
const { event } = req.body;
console.log(`Received event: ${event}`);
res.status(200).send('OK');
});
app.listen(3000);
Python (Flask)
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET_KEY = b'sk_live_...' # Your secret key as bytes
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-CreptaPay-Signature')
if not signature:
return 'Missing signature', 400
payload = request.get_json()
data_payload = payload.get('data')
# Convert the data payload back to a normalized JSON string
serialized_data = json.dumps(data_payload, separators=(',', ':'))
# Calculate HMAC SHA-256
calculated_signature = hmac.new(
SECRET_KEY,
msg=serialized_data.encode('utf-8'),
digestmod=hashlib.sha256
).hexdigest()
# Perform constant-time comparison
if not hmac.compare_digest(signature, calculated_signature):
return 'Invalid signature', 401
# Handle the verified event
event = payload.get('event')
print(f"Received event: {event}")
return 'OK', 200
if __name__ == '__main__':
app.run(port=3000)
PHP
<?php
$secretKey = 'sk_live_...';
$signature = $_SERVER['HTTP_X_CREPTAPAY_SIGNATURE'] ?? null;
if (!$signature) {
http_response_code(400);
exit('Missing signature');
}
// Retrieve the raw request body
$rawPayload = file_get_contents('php://input');
$payload = json_decode($rawPayload, true);
$dataPayload = $payload['data'] ?? null;
// Re-serialize the inner data object to guarantee exact payload matching
$serializedData = json_encode($dataPayload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Calculate HMAC SHA-256
$calculatedSignature = hash_hmac('sha256', $serializedData, $secretKey);
// Perform constant-time comparison
if (!hash_equals($signature, $calculatedSignature)) {
http_response_code(401);
exit('Invalid signature');
}
// Handle the verified event
$event = $payload['event'] ?? '';
error_log("Received event: " . $event);
http_response_code(200);
echo 'OK';
Java (Spring Boot)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.json.JSONObject;
@RestController
public class WebhookController {
private static final String SECRET_KEY = "sk_live_...";
private static final String HMAC_SHA256 = "HmacSHA256";
@PostMapping("/webhook")
public ResponseEntity<String> handleWebhook(
@RequestBody String rawBody,
@RequestHeader("X-CreptaPay-Signature") String signature) {
if (signature == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Missing signature");
}
try {
// Parse payload to retrieve the inner data object
JSONObject jsonPayload = new JSONObject(rawBody);
JSONObject dataObject = jsonPayload.getJSONObject("data");
String serializedData = dataObject.toString();
// Calculate HMAC SHA-256
Mac sha256Hmac = Mac.getInstance(HMAC_SHA256);
SecretKeySpec secretKeySpec = new SecretKeySpec(
SECRET_KEY.getBytes(StandardCharsets.UTF_8),
HMAC_SHA256
);
sha256Hmac.init(secretKeySpec);
byte[] hashBytes = sha256Hmac.doFinal(serializedData.getBytes(StandardCharsets.UTF_8));
// Convert signature to hex string
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
String calculatedSignature = hexString.toString();
// Perform constant-time comparison
boolean isValid = MessageDigest.isEqual(
signature.getBytes(StandardCharsets.UTF_8),
calculatedSignature.getBytes(StandardCharsets.UTF_8)
);
if (!isValid) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid signature");
}
// Handle the verified event
String event = jsonPayload.getString("event");
System.out.println("Received event: " + event);
return ResponseEntity.ok("OK");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error verifying signature");
}
}
}