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.
Register your endpoint within Axicloud using the Webhooks Dashboard or the API.
Users need to create an HTTP endpoint (e.g., /events) that listens for POST requests, as webhook events are sent via HTTP POST.
Request Verification:
To ensure that the incoming request is from the Axicloud Webhook Service, verify the request signature. The signature is provided in the X-AW-Signature header.
Handling Signature Verification:
Implement a function (for example, generateSignature) that takes the HTTP method, URL, timestamp, and raw body of the request, and generates a signature using a shared API secret.
Important: The signature function expects the URL to include only the path and query string (if any), not the full URL with the domain.
For instance, if your full URL is:
https://example.com/events?foo=bar
you should use:
/events?foo=bar
in your signature calculation.
Compare the generated signature with the one provided in the request header.
Returning 2XX Status Code:
To signal successful receipt of the webhook event, return a 2XX status code (e.g., res.sendStatus(200) in Node.js). This informs the Axicloud Webhook Service that the event was successfully received and prevents unnecessary retries.
Once the signature is validated, you can trust that the incoming webhook event is legitimate.
Processing the Event:
Process the event according to the payload provided in the request body (e.g., req.body in Node.js or $webhook in PHP). The payload structure depends on the event type triggered by the Axicloud Webhook Service.
Idempotent Endpoint Design:
Design your webhook endpoint to be idempotent. This means that if the same event is received more than once (due to retries, for example), it won’t cause unintended side effects or duplicate data.
You need to implement HTTP endpoints that accept POST requests, validate the request signature (using only the URL’s path and query), process the specific event, and return a 2XX status code to acknowledge successful receipt. Designing idempotent endpoints helps prevent issues related to duplicate data.
It's important to note that the specific implementation details may vary based on the programming language and framework used, but the overall approach remains consistent. Refer to the Axicloud Webhook Service documentation for event-specific payload details and any additional requirements.
Below are several code examples that illustrate how to implement signature verification. Notice that in each example the URL used for signature generation consists of only the path and query string (if any).
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 calculatedHmac = BitConverter.ToString(hmac.ComputeHash(Encoding.UTF8.GetBytes(dataToSign))).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);
}
}
}
}
That's it! You can now receive Axitech Webhooks Events in your app ✨