ATLAS
ATLAS
  • Docs
  • Changelog
  • Feature requests
  • Support portal
    • Webhook Authentication
Docs / Webhooks

Webhook Authentication

ATLAS Webhooks are powered by Hook0, an open-source webhook infrastructure service. To ensure that incoming webhook events are genuine and originate from ATLAS, your application must verify the signature included with every webhook delivery. ATLAS signs each webhook using HMAC-SHA256 and sends the signature in the X-Hook0-Signature HTTP header.

Webhook requests must not be processed until this signature is verified.


1. How ATLAS Signs Webhooks (via Hook0)

For every webhook event:

  1. Hook0 generates:

  • a timestamp (t)

  • a list of headers used in the signature (h)

  • the raw JSON body

  1. Hook0 constructs a signed_payload:

   signed_payload =
       timestamp + "." +
       headers_list + "." +
       header_values_joined + "." +
       raw_body
  1. Hook0 computes:

   HMAC_SHA256(signed_payload, SIGNING_SECRET)
  1. The resulting signature is included in the header:

   X-Hook0-Signature: t=...,h=...,v1=...

Only the v1 signature needs to be validated.


2. Your Responsibility: Verifying the Signature

When your server receives a webhook:

Step 1 — Read Signature Header

Read the entire X-Hook0-Signature header and extract:

  • t (timestamp)

  • h (space-separated list of headers)

  • v1 (the signature)

Example:

t=1733399090,h=X-Event-Id X-Event-Type,v1=abcdef123456...

Step 2 — Rebuild the Signed Payload

  1. From the value of h, split the header names.

  2. Read each of these headers from the incoming HTTP request in the same order.

  3. Join these values with "." to produce header_values_joined.

  4. Concatenate:

signed_payload = t + "." + h + "." + header_values_joined + "." + raw_body

Important: Use the exact raw body as received (no formatting changes, whitespace changes, or re-serialization).


Step 3 — Compute Your Own HMAC

Using the signing secret from your Hook0 subscription:

expected_signature = HMAC_SHA256(signed_payload, SIGNING_SECRET)

Hex-encode the result.


Step 4 — Compare Signatures (Constant Time)

Compare:

  • expected_signature

  • v1 from X-Hook0-Signature

Use a constant-time comparison to prevent timing attacks.

If they do not match → reject the request.


Step 5 — Timestamp Check (Replay Protection)

Convert t to a UNIX timestamp and ensure:

now - t < 5 minutes

If the timestamp is too old → reject as a possible replay attack.


3. Summary

Before processing any webhook event, your server must:

  1. Parse X-Hook0-Signature

  2. Rebuild signed_payload

  3. Compute HMAC-SHA256 using your signing secret

  4. Compare with v1 (constant time)

  5. Validate timestamp freshness

A webhook should be trusted only if all the steps above succeed.


4. Code Examples

.NET (C#)

using System.Security.Cryptography;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("webhooks/hook0")]
public class Hook0Webhook : ControllerBase
{
    private readonly string secret = "YOUR_SIGNING_SECRET";

    [HttpPost]
    public async Task<IActionResult> Receive()
    {
        var sigHeader = Request.Headers["X-Hook0-Signature"].ToString();
        if (string.IsNullOrEmpty(sigHeader)) return Unauthorized();

        string timestamp = null, headersList = null, sigV1 = null;

        foreach (var part in sigHeader.Split(','))
        {
            var kv = part.Split('=');
            if (kv.Length != 2) continue;

            if (kv[0] == "t") timestamp = kv[1];
            if (kv[0] == "h") headersList = kv[1];
            if (kv[0] == "v1") sigV1 = kv[1];
        }

        Request.EnableBuffering();
        string body;
        using (var reader = new StreamReader(Request.Body, leaveOpen:true))
            body = await reader.ReadToEndAsync();
        Request.Body.Position = 0;

        var headerNames = headersList.Split(' ');
        var headerValues = headerNames.Select(h => Request.Headers[h].ToString());
        var headerJoin = string.Join('.', headerValues);

        var signedPayload = $"{timestamp}.{headersList}.{headerJoin}.{body}";

        var expected = new HMACSHA256(System.Text.Encoding.UTF8.GetBytes(secret))
            .ComputeHash(System.Text.Encoding.UTF8.GetBytes(signedPayload));

        var expectedHex = Convert.ToHexString(expected).ToLower();

        if (!CryptographicOperations.FixedTimeEquals(
            System.Text.Encoding.UTF8.GetBytes(expectedHex),
            System.Text.Encoding.UTF8.GetBytes(sigV1)))
            return Unauthorized();

        return Ok();
    }
}

Node.js

const crypto = require("crypto");

function verifyHook0(req) {
  const sigHeader = req.headers["x-hook0-signature"];
  if (!sigHeader) return false;

  const parts = Object.fromEntries(
    sigHeader.split(",").map(p => p.split("="))
  );

  const timestamp = parts.t;
  const headersList = parts.h;
  const signature = parts.v1;

  const headerNames = headersList.split(" ");
  const headerValues = headerNames.map(h => req.headers[h.toLowerCase()]);
  const headerJoined = headerValues.join(".");

  const signedPayload =
    `${timestamp}.${headersList}.${headerJoined}.${req.rawBody}`;

  const expected = crypto
    .createHmac("sha256", process.env.HOOK0_SECRET)
    .update(signedPayload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

Python

import hmac
import hashlib

def verify_hook0(request, secret):
    sig_header = request.headers.get("X-Hook0-Signature")
    if not sig_header:
        return False

    parts = dict(p.split("=") for p in sig_header.split(","))

    timestamp = parts["t"]
    headers_list = parts["h"]
    signature = parts["v1"]

    header_names = headers_list.split(" ")
    header_values = [request.headers.get(h) for h in header_names]
    header_joined = ".".join(header_values)

    signed_payload = (
        f"{timestamp}.{headers_list}.{header_joined}."
        f"{request.data.decode()}"
    )

    expected = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)
PrevAccounting Integration Guide
NextQuery
Was this helpful?