Webhook Security
Webhook Signature Verification
Our webhooks include cryptographic signatures to verify authenticity.
Headers
x-ogateway-signature: HMAC-SHA512 signature (hex encoded)
Verification Steps
- Extract
x-ogateway-signatureheader - Read the raw request body (before parsing JSON)
- Compute HMAC-SHA512 of the raw body using your secret key
- Compare computed signature with received signature (timing-safe comparison)
Secret Key
Your webhook secret (WEBHOOK_SECRET): your_api_key
Merchant Implementation Examples
app.post("/my/webhook/url", function(req, res) {
try {
const signature = req.headers['x-ogateway-signature'];
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
// Sign raw body (use body-parser with verify option to preserve raw body)
// Alternatively: const payload = JSON.stringify(req.body);
const payload = req.rawBody || JSON.stringify(req.body);
const hash = crypto
.createHmac('sha512', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
// Timing-safe comparison
const hashBuffer = Buffer.from(hash, 'hex');
const signatureBuffer = Buffer.from(signature, 'hex');
if (hashBuffer.length !== signatureBuffer.length ||
!crypto.timingSafeEqual(hashBuffer, signatureBuffer)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Signature valid - process event
const event = req.body;
// Process your webhook event here
return res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
});import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;
public class WebhookServer {
private static final String WEBHOOK_SECRET = System.getenv("WEBHOOK_SECRET");
public static void main(String[] args) throws IOException {
int port = 8080;
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/my/webhook/url", new WebhookHandler());
server.setExecutor(null); // Use default executor
server.start();
System.out.println("Server started on port " + port);
}
static class WebhookHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
// Only accept POST requests
if (!"POST".equals(exchange.getRequestMethod())) {
sendResponse(exchange, 405, "{\"error\":\"Method not allowed\"}");
return;
}
try {
// Get headers
String signature = getHeader(exchange, "x-ogateway-signature");
if (signature == null || signature.isEmpty()) {
sendResponse(exchange, 401, "{\"error\":\"Missing signature\"}");
return;
}
// Read raw body
String rawBody = readRequestBody(exchange);
// Compute HMAC
Mac hmac = Mac.getInstance("HmacSHA512");
SecretKeySpec secretKey = new SecretKeySpec(
WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8),
"HmacSHA512"
);
hmac.init(secretKey);
byte[] hash = hmac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
// Convert to hex
String computedSignature = bytesToHex(hash);
// Timing-safe comparison
if (!MessageDigest.isEqual(
computedSignature.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8))) {
sendResponse(exchange, 401, "{\"error\":\"Invalid signature\"}");
return;
}
// Signature valid - process event
// Parse rawBody as JSON and process your webhook event here
System.out.println("Valid webhook received: " + rawBody);
sendResponse(exchange, 200, "{\"received\":true}");
} catch (Exception e) {
e.printStackTrace();
sendResponse(exchange, 500, "{\"error\":\"Internal server error\"}");
}
}
private String getHeader(HttpExchange exchange, String headerName) {
List<String> values = exchange.getRequestHeaders().get(headerName);
return (values != null && !values.isEmpty()) ? values.get(0) : null;
}
private String readRequestBody(HttpExchange exchange) throws IOException {
InputStream is = exchange.getRequestBody();
byte[] bytes = is.readAllBytes();
return new String(bytes, StandardCharsets.UTF_8);
}
private void sendResponse(HttpExchange exchange, int statusCode, String response)
throws IOException {
exchange.getResponseHeaders().set("Content-Type", "application/json");
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(statusCode, bytes.length);
OutputStream os = exchange.getResponseBody();
os.write(bytes);
os.close();
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
}import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
@RestController
public class WebhookController {
private static final String WEBHOOK_SECRET = System.getenv("WEBHOOK_SECRET");
@PostMapping("/my/webhook/url")
public ResponseEntity<?> handleWebhook(
@RequestBody String rawBody,
@RequestHeader(value = "x-ogateway-signature", required = false) String signature) {
try {
// Validate headers
if (signature == null || signature.isEmpty()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Missing signature"));
}
// Compute HMAC
Mac hmac = Mac.getInstance("HmacSHA512");
SecretKeySpec secretKey = new SecretKeySpec(
WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8),
"HmacSHA512"
);
hmac.init(secretKey);
byte[] hash = hmac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
// Convert to hex
String computedSignature = bytesToHex(hash);
// Timing-safe comparison
if (!MessageDigest.isEqual(
computedSignature.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8))) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Invalid signature"));
}
// Signature valid - process event
// Parse rawBody as JSON and process
return ResponseEntity.ok(Map.of("received", true));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Internal server error"));
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}import hmac
import hashlib
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', '').encode('utf-8')
@app.route('/my/webhook/url', methods=['POST'])
def handle_webhook():
try:
# Get headers
signature = request.headers.get('x-ogateway-signature', '')
if not signature:
return jsonify({'error': 'Missing signature'}), 401
# Get raw body
raw_body = request.get_data(as_text=True)
# Compute HMAC
computed_signature = hmac.new(
WEBHOOK_SECRET,
raw_body.encode('utf-8'),
hashlib.sha512
).hexdigest()
# Timing-safe comparison
if not hmac.compare_digest(computed_signature, signature):
return jsonify({'error': 'Invalid signature'}), 401
# Signature valid - process event
event = request.get_json()
# Process your webhook event here
return jsonify({'received': True}), 200
except Exception as e:
print(f"Webhook error: {e}")
return jsonify({'error': 'Internal server error'}), 500
if __name__ == '__main__':
app.run()import hmac
import hashlib
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
import os
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', '').encode('utf-8')
@csrf_exempt
@require_http_methods(["POST"])
def handle_webhook(request):
try:
signature = request.headers.get('x-ogateway-signature', '')
if not signature:
return JsonResponse({'error': 'Missing signature'}, status=401)
# Get raw body
raw_body = request.body.decode('utf-8')
# Compute HMAC
computed_signature = hmac.new(
WEBHOOK_SECRET,
raw_body.encode('utf-8'),
hashlib.sha512
).hexdigest()
# Timing-safe comparison
if not hmac.compare_digest(computed_signature, signature):
return JsonResponse({'error': 'Invalid signature'}, status=401)
# Process event
event = json.loads(raw_body)
# Process your webhook event here
return JsonResponse({'received': True}, status=200)
except Exception as e:
return JsonResponse({'error': 'Internal server error'}, status=500)package main
import (
"crypto/hmac"
"crypto/sha512"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type ErrorResponse struct {
Error string `json:"error"`
}
type SuccessResponse struct {
Received bool `json:"received"`
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
// Only accept POST
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Get headers
signature := r.Header.Get("x-ogateway-signature")
if signature == "" {
respondJSON(w, http.StatusUnauthorized, ErrorResponse{Error: "Missing signature"})
return
}
// Read raw body
body, err := io.ReadAll(r.Body)
if err != nil {
respondJSON(w, http.StatusInternalServerError, ErrorResponse{Error: "Failed to read body"})
return
}
defer r.Body.Close()
// Get secret from environment
secret := os.Getenv("WEBHOOK_SECRET")
if secret == "" {
respondJSON(w, http.StatusInternalServerError, ErrorResponse{Error: "Secret not configured"})
return
}
// Compute HMAC
mac := hmac.New(sha512.New, []byte(secret))
mac.Write(body)
computedSignature := hex.EncodeToString(mac.Sum(nil))
// Timing-safe comparison
if subtle.ConstantTimeCompare([]byte(computedSignature), []byte(signature)) != 1 {
respondJSON(w, http.StatusUnauthorized, ErrorResponse{Error: "Invalid signature"})
return
}
// Signature valid - process event
var event map[string]interface{}
if err := json.Unmarshal(body, &event); err != nil {
respondJSON(w, http.StatusBadRequest, ErrorResponse{Error: "Invalid JSON"})
return
}
// Process your webhook event here
respondJSON(w, http.StatusOK, SuccessResponse{Received: true})
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func main() {
http.HandleFunc("/my/webhook/url", handleWebhook)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
fmt.Printf("Server listening on port %s\n", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
fmt.Printf("Error starting server: %v\n", err)
}
}require 'sinatra'
require 'json'
require 'openssl'
require 'rack'
WEBHOOK_SECRET = ENV['WEBHOOK_SECRET'] || ''
# Middleware to capture raw body
class RawBodyCapture
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
env['rack.input'].rewind
env['raw_body'] = request.body.read
env['rack.input'].rewind
@app.call(env)
end
end
use RawBodyCapture
post '/my/webhook/url' do
begin
content_type :json
# Get headers
signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']
if signature.nil? || signature.empty?
status 401
return { error: 'Missing signature' }.to_json
end
# Get raw body
raw_body = request.env['raw_body']
# Compute HMAC
computed_signature = OpenSSL::HMAC.hexdigest(
OpenSSL::Digest.new('sha512'),
WEBHOOK_SECRET,
raw_body
)
# Timing-safe comparison
unless Rack::Utils.secure_compare(computed_signature, signature)
status 401
return { error: 'Invalid signature' }.to_json
end
# Signature valid - process event
event = JSON.parse(raw_body)
# Process your webhook event here
status 200
{ received: true }.to_json
rescue JSON::ParserError
status 400
{ error: 'Invalid JSON' }.to_json
rescue StandardError => e
puts "Webhook error: #{e.message}"
status 500
{ error: 'Internal server error' }.to_json
end
end# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
WEBHOOK_SECRET = ENV['WEBHOOK_SECRET'] || ''
def receive
signature = request.headers['x-ogateway-signature']
if signature.blank?
render json: { error: 'Missing signature' }, status: :unauthorized
return
end
# Get raw body
raw_body = request.raw_post
# Compute HMAC
computed_signature = OpenSSL::HMAC.hexdigest(
OpenSSL::Digest.new('sha512'),
WEBHOOK_SECRET,
raw_body
)
# Timing-safe comparison
unless ActiveSupport::SecurityUtils.secure_compare(computed_signature, signature)
render json: { error: 'Invalid signature' }, status: :unauthorized
return
end
# Process event
event = JSON.parse(raw_body)
# Process your webhook event here
render json: { received: true }, status: :ok
rescue JSON::ParserError
render json: { error: 'Invalid JSON' }, status: :bad_request
rescue StandardError => e
Rails.logger.error "Webhook error: #{e.message}"
render json: { error: 'Internal server error' }, status: :internal_server_error
end
endUpdated 18 days ago