Webhook Security

Webhook Signature Verification

Our webhooks include cryptographic signatures to verify authenticity.

Headers

  • x-ogateway-signature: HMAC-SHA512 signature (hex encoded)

Verification Steps

  1. Extract x-ogateway-signature header
  2. Read the raw request body (before parsing JSON)
  3. Compute HMAC-SHA512 of the raw body using your secret key
  4. 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
end