> ## Documentation Index
> Fetch the complete documentation index at: https://docs.creptapay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify Signature

> Secure your webhook endpoint by verifying incoming signatures from CreptaPay

To ensure webhook events received at your endpoint are actually sent by CreptaPay and have not been tampered with, you must verify the signature sent in the request headers.

CreptaPay includes an `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

1. Retrieve the `X-CreptaPay-Signature` header from the incoming request.
2. Extract the `data` object from the request body.
3. Generate a SHA-256 HMAC of the stringified `data` object using your secret API key as the key.
4. Compare your generated signature with the signature retrieved from the header.

<Warning>
  You must perform a constant-time comparison to prevent timing attacks.
</Warning>

## Code examples

Here is how to verify webhook signatures in different languages:

### Node.js (Express)

```javascript theme={null}
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)

```python theme={null}
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 theme={null}
<?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)

```java theme={null}
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");
        }
    }
}
```
