Setup

How to set up your webhook integration


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.
  • Secure your webhook endpoint.

Step 1: Create Your Webhook Endpoint

  1. Create an HTTP Endpoint:
    • Set up an endpoint (e.g., /events) in your application that can receive POST requests
    • All webhook events will be sent as HTTP POST requests to your endpoint
  2. Implement Request Verification:
    • Every webhook request includes a signature in the X-AW-Signature header
    • You must verify this signature to ensure the request came from Axicloud
    • The signature is generated using your webhook secret and the request details
  3. Return Success Response:
    • Return a 2XX status code (e.g., 200 or 204) to acknowledge receipt
    • This prevents unnecessary retries and confirms successful delivery

Step 2: Implement Signature Verification

The signature verification process ensures webhook security:

  1. Get Required Values:
    • HTTP method (e.g., "POST")
    • URL path and query string (e.g., "/events?foo=bar")
    • Timestamp from X-AW-Timestamp header
    • Raw request body
    • Signature from X-AW-Signature header
  2. Important URL Note: The signature function expects only the path and query string, not the full URL:
    ✅ Use: "/events?foo=bar"
    ❌ Not: "https://example.com/events?foo=bar"
    
  3. 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);
            }
        }
    }
}

Step 3: Process the Webhook

Once you've verified the signature, you can process the webhook data:

  1. Parse the Payload:
    • The request body contains the event data in JSON format
    • The structure depends on the event type (see Events)
  2. Handle Idempotency:
    • Design your processing logic to be idempotent
    • The same event might be delivered multiple times due to retries
    • Use event IDs to prevent duplicate processing
  3. Process Asynchronously:
    • Return a 2XX response quickly
    • Process the event data in the background if needed
    • This prevents timeouts and unnecessary retries

Webhook Response Handling

Your endpoint should respond appropriately to inform our service about the delivery status:

Expected Response Codes

  • 2XX Status Codes: Success - Event received and will be processed
    • 200 OK: Standard success response
    • 204 No Content: Success with no response body needed
  • 4XX Status Codes: Client errors - Won't retry
    • 400 Bad Request: Invalid payload
    • 401 Unauthorized: Invalid signature
    • 403 Forbidden: Lack of permissions
    • 404 Not Found: Invalid endpoint
    • 429 Too Many Requests: Rate limit exceeded
  • 5XX Status Codes: Server errors - Will retry
    • 500 Internal Server Error: Generic server error
    • 502 Bad Gateway: Invalid upstream response
    • 503 Service Unavailable: Server temporarily down
    • 504 Gateway Timeout: Request timeout

Retry Behavior

Our service implements automatic retries for failed deliveries:

  1. Failed Deliveries:
    • Non-2XX responses trigger retries
    • Network failures trigger retries
    • Timeouts trigger retries
  2. Retry Strategy:
    • Maximum 5 retry attempts
    • Exponential backoff between attempts
    • Increasing delays prevent system overload
  3. Permanent Failures:
    • After all retries exhausted, event marked as failed
    • No further delivery attempts made
    • Failed events viewable in dashboard

Best Practices

  1. Quick Response:
    • Return 2XX quickly
    • Process events asynchronously
    • Avoid timeouts
  2. Idempotency:
    • Handle duplicate events gracefully
    • Use event IDs for deduplication
    • Store processed event IDs
  3. Error Handling:
    • Log all webhook errors
    • Monitor webhook health
    • Set up alerts for failures
  4. Security:
    • Always verify signatures
    • Use HTTPS endpoints
    • Keep webhook secrets secure
This implementation follows standard webhook handling practices used by major service providers:
That's it! You can now receive Axitech Webhooks Events in your app ✨