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.
  • Register your endpoint within Axicloud using the Webhooks Dashboard or the API.
  • Secure your webhook endpoint.

Implementing Endpoints to Receive Webhook Events

  1. HTTP Endpoint Setup:
    • Users need to create an HTTP endpoint (e.g., /events) that listens for POST requests, as webhook events are sent via HTTP POST.
  2. 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.
  3. 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.
  4. 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.

Handling Webhook Events

  1. Validating Request Signature:
    • Once the signature is validated, you can trust that the incoming webhook event is legitimate.
  2. 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.
  3. 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.

Summary

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.

Example

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 ✨