To start receiving webhook events in your app, create and register a webhook endpoint by following the steps below. You can register and create one endpoint to handle several different event types at once, or set up individual endpoints for specific events.
Identify which events you want to monitor.
Develop a webhook endpoint function to receive event data via POST requests.
Talk to your Axitech Representative to set up your webhook endpoint and get the webhook secret.
Verify the Signature:
Choose your programming language below for implementation examples:
Node.js (Javascript)
const crypto = require("crypto");
const express = require("express");
const API_SECRET = "wh_sec_YOUR_WEBHOOK_SECRET";
const app = express();
app.use(
express.json({
verify: (req, res, buffer) => {
req.rawBody = buffer;
},
})
);
app.post("/", (req, res) => {
// In Express, req.url contains only the path and query string.
const signature = generateSignature(
req.method,
req.url, // Only path and query are used!
req.headers["x-aw-timestamp"],
req.rawBody
);
if (signature !== req.headers["x-aw-signature"]) {
return res.sendStatus(401);
}
console.log("Received webhook", req.body);
res.sendStatus(200);
});
app.listen(9000, () => console.log("Node.js server started on port 9000."));
function generateSignature(method, url, timestamp, body) {
const hmac = crypto.createHmac("SHA256", API_SECRET);
// Only the URL path and query are used in the signature calculation.
hmac.update(`${method.toUpperCase()}${url}${timestamp}`);
if (body) {
hmac.update(body);
}
return hmac.digest("hex");
}
Node.js (Typescript)
import * as crypto from "crypto";
import * as express from "express";
const API_SECRET: string = "secret";
const app: express.Application = express();
app.use(
express.json({
verify: (req: express.Request, res: express.Response, buffer: Buffer) => {
req.rawBody = buffer;
},
})
);
app.post("/", (req: express.Request, res: express.Response) => {
// Note: req.url here is only the path and query.
const signature: string = generateSignature(
req.method,
req.url,
req.headers["x-aw-timestamp"] as string,
req.rawBody
);
if (signature !== req.headers["x-aw-signature"]) {
return res.sendStatus(401);
}
console.log("Received webhook", req.body);
res.sendStatus(200);
});
app.listen(9000, () => console.log("Node.js server started on port 9000."));
function generateSignature(
method: string,
url: string,
timestamp: string,
body: Buffer
): string {
const hmac: crypto.Hmac = crypto.createHmac("SHA256", API_SECRET);
// The signature uses only the URL's path and query.
hmac.update(`${method.toUpperCase()}${url}${timestamp}`);
if (body) {
hmac.update(body);
}
return hmac.digest("hex");
}
PHP
<?php
$apiSecret = 'secret';
// Retrieve HTTP method, headers, and raw body
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? '';
$timestamp = $_SERVER['HTTP_X_AW_TIMESTAMP'] ?? '';
// $_SERVER['REQUEST_URI'] returns only the path and query string.
$url = $_SERVER['REQUEST_URI'];
$body = file_get_contents('php://input');
// Generate signature
$signature = generateSignature($requestMethod, $url, $timestamp, $body);
// Verify signature
if (!hash_equals($signature, $_SERVER['HTTP_X_AW_SIGNATURE'] ?? '')) {
http_response_code(401);
die();
}
// Process webhook event
$webhook = json_decode($body);
file_put_contents('php://stdout', 'Webhook event received: ' . print_r($webhook, true) . PHP_EOL);
// Respond with a 2XX status code
http_response_code(200);
function generateSignature($method, $url, $timestamp, $body) {
global $apiSecret;
$data = $method . $url . $timestamp . $body;
return hash_hmac('sha256', $data, $apiSecret);
}
Laravel
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class WebhookController extends Controller
{
private $apiSecret = 'secret';
public function handleWebhook(Request $request)
{
// Use getRequestUri() to include the query string, not just the path.
$url = $request->getRequestUri();
$signature = $this->generateSignature(
$request->method(),
$url, // Only path and query are used.
$request->header('x-aw-timestamp'),
$request->getContent()
);
// Verify signature
if (!hash_equals($signature, $request->header('x-aw-signature'))) {
return response()->json(['error' => 'Invalid webhook signature'], 401);
}
// Process webhook event
$webhook = json_decode($request->getContent(), true);
\Log::info('Webhook event received: ' . print_r($webhook, true));
// Respond with a 2XX status code
return response()->json(['message' => 'Webhook event received'], 200);
}
private function generateSignature($method, $url, $timestamp, $body)
{
$data = $method . $url . $timestamp . $body;
return hash_hmac('sha256', $data, $this->apiSecret);
}
}
Golang
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
)
const apiSecret = "secret"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
// r.URL.RequestURI() returns the path and query string.
method := r.Method
url := r.URL.RequestURI() // Only path and query are used.
timestamp := r.Header.Get("X-AW-Timestamp")
receivedHmac := r.Header.Get("X-AW-Signature")
// Verify signature
if !verifySignature(method, url, timestamp, body, receivedHmac) {
http.Error(w, "Invalid webhook signature", http.StatusUnauthorized)
return
}
// Process webhook event
fmt.Println("Webhook event received:", string(body))
// Respond with a 2XX status code
w.WriteHeader(http.StatusOK)
})
fmt.Println("Go server started on port 9000.")
http.ListenAndServe(":9000", nil)
}
func verifySignature(method, url, timestamp string, body []byte, receivedHmac string) bool {
h := hmac.New(sha256.New, []byte(apiSecret))
h.Write([]byte(fmt.Sprintf("%s%s%s", method, url, timestamp)))
h.Write(body)
calculatedHmac := hex.EncodeToString(h.Sum(nil))
return hmac.Equal([]byte(calculatedHmac), []byte(receivedHmac))
}
Ruby
require 'sinatra'
require 'openssl'
require 'json'
set :port, 9000
API_SECRET = 'secret'
before do
request.body.rewind
@request_payload = request.body.read
end
post '/' do
# request.fullpath returns only the path and query string.
method = request.request_method
url = request.fullpath # Only path and query are used.
timestamp = request.env['HTTP_X_AW_TIMESTAMP']
received_hmac = request.env['HTTP_X_AW_SIGNATURE']
# Verify signature
unless verify_signature(method, url, timestamp, @request_payload, received_hmac)
status 401
return 'Invalid webhook signature'
end
# Process webhook event
webhook = JSON.parse(@request_payload)
puts "Webhook event received: #{webhook}"
# Respond with a 2XX status code
status 200
end
def verify_signature(method, url, timestamp, body, received_hmac)
calculated_hmac = OpenSSL::HMAC.hexdigest('sha256', API_SECRET, "#{method}#{url}#{timestamp}#{body}")
calculated_hmac == received_hmac
end
Python
from flask import Flask, request, abort
import hmac
import hashlib
import json
app = Flask(__name__)
API_SECRET = 'secret'
@app.route('/', methods=['POST'])
def webhook():
try:
raw_body = request.get_data()
method = request.method
# Use request.full_path to get only the path and query (excluding domain)
url = request.full_path # Only path and query are used.
timestamp = request.headers.get('X-AW-Timestamp')
received_hmac = request.headers.get('X-AW-Signature')
# Verify signature
if not verify_signature(method, url, timestamp, raw_body, received_hmac):
abort(401, 'Invalid webhook signature')
# Process webhook event
webhook_data = json.loads(raw_body)
print('Webhook event received:', webhook_data)
# Respond with a 2XX status code
return '', 200
except Exception as e:
print('Error processing webhook:', str(e))
abort(500, 'Internal Server Error')
def verify_signature(method, url, timestamp, body, received_hmac):
data_to_sign = f'{method}{url}{timestamp}{body.decode("utf-8") if body else ""}'
calculated_hmac = hmac.new(API_SECRET.encode('utf-8'), data_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()
return hmac.compare_digest(calculated_hmac, received_hmac)
if __name__ == '__main__':
app.run(port=9000)
.NET
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
class Program
{
static void Main()
{
var builder = new WebHostBuilder()
.UseKestrel()
.Configure(app =>
{
app.Use(async (context, next) =>
{
using (var reader = new StreamReader(context.Request.Body))
{
var rawBody = await reader.ReadToEndAsync();
var method = context.Request.Method;
// Combine Path and QueryString to use only path and query.
var url = context.Request.Path + context.Request.QueryString;
var timestamp = context.Request.Headers["X-AW-Timestamp"];
var receivedHmac = context.Request.Headers["X-AW-Signature"];
// Verify signature
if (!VerifySignature(method, url, timestamp, rawBody, receivedHmac))
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
// Process webhook event
Console.WriteLine($"Webhook event received: {rawBody}");
// Respond with a 2XX status code
context.Response.StatusCode = (int)HttpStatusCode.OK;
}
});
});
var host = builder.Build();
host.Run();
}
static bool VerifySignature(string method, string url, string timestamp, string body, string receivedHmac)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes("secret")))
{
var dataToSign = $"{method}{url}{timestamp}{body}";
var calculatedHmacBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(dataToSign));
var calculatedHmac = BitConverter.ToString(calculatedHmacBytes).Replace("-", "").ToLower();
return string.Equals(calculatedHmac, receivedHmac, StringComparison.OrdinalIgnoreCase);
}
}
}
Java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@SpringBootApplication
public class WebhookApplication {
private static final String API_SECRET = "secret";
public static void main(String[] args) {
SpringApplication.run(WebhookApplication.class, args);
}
@RestController
public static class WebhookController {
@PostMapping("/")
public void handleWebhook(
@RequestHeader("X-AW-Timestamp") String timestamp,
@RequestHeader("X-AW-Signature") String receivedHmac,
@RequestBody String body
) {
// For this example, the URL is fixed as "/" (only path and query).
if (!verifySignature("POST", "/", timestamp, body, receivedHmac)) {
throw new SecurityException("Invalid webhook signature");
}
// Process webhook event
System.out.println("Webhook event received: " + body);
// Respond with a 2XX status code
}
private boolean verifySignature(String method, String url, String timestamp, String body, String receivedHmac) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(API_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
// Note: url should include only the path and query.
String dataToSign = method + url + timestamp + body;
byte[] calculatedHmacBytes = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));
String calculatedHmac = javax.xml.bind.DatatypeConverter.printHexBinary(calculatedHmacBytes).toLowerCase();
return calculatedHmac.equals(receivedHmac);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Error verifying signature", e);
}
}
}
}