Skip to content

Webhooks

Overview

Integrators can be notified of completed async and registered queries via webhooks, once the feature has been configured for their subscription. This removes the need to poll Halo Cloud to check for new results.

Webhooks are available for:

  • Async queries
  • Registered queries

For security reasons, the webhook payload is intentionally minimal and does not include the query result. To retrieve results, use the appropriate API endpoint with the queryId provided in the payload.

How It Works

When an async or registered query completes successfully, a webhook notification is sent to the integrator's configured URL:

  1. The query completion event is published to an internal message queue.
  2. The webhook processor picks up the event, constructs the payload, signs it with HMAC-SHA256, and sends it as an HTTPS POST to the integrator's URL.
  3. If delivery fails, the system retries automatically using exponential backoff.

Webhook Payload

All webhooks are sent as an HTTPS POST with a Content-Type: application/json header.

Payload Fields

Field Type Description
id string UUID — the unique ID of this webhook notification.
occurredAt string ISO 8601 timestamp — when the query completed and the webhook was queued.
siteId string UUID — the Halo site the query was sent to.
queryId string UUID — the ID of the completed query.
webhookSource string The source query type: "async" or "registered".

Sample Payload

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "occurredAt": "2026-03-05T14:30:00.0000000+00:00",
  "siteId": "12345678-abcd-ef01-2345-6789abcdef01",
  "queryId": "abcdef01-2345-6789-abcd-ef0123456789",
  "webhookSource": "async"
}

Note: The payload does not include the query result. To retrieve the result, use the appropriate API endpoint with the queryId.

Webhook Headers

Each webhook request includes the following custom headers:

Header Description
X-Halo-Id The webhook's unique ID. Same as the id field in the payload.
X-Halo-Timestamp ISO 8601 timestamp of when the webhook was sent. Used for replay prevention and HMAC verification.
X-Halo-Signature-256 HMAC-SHA256 signature for verifying payload integrity and authenticity.

HMAC Signature Verification

Each webhook includes an HMAC-SHA256 signature in the X-Halo-Signature-256 header. This allows integrators to verify that the webhook originated from Halo and has not been tampered with.

Using the signature is optional, but it is included with every webhook. A unique secret is provisioned per integrator when webhooks are configured.

Signature Construction

The signature is computed as follows:

  1. Construct the signed message by concatenating the raw JSON request body, a literal . (period), and the X-Halo-Timestamp header value:
<json_body>.<X-Halo-Timestamp>

For example, given the sample payload above and a timestamp of 2026-03-05T14:30:01.1234567+00:00:

{"id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","occurredAt":"2026-03-05T14:30:00+00:00","siteId":"12345678-abcd-ef01-2345-6789abcdef01","queryId":"abcdef01-2345-6789-abcd-ef0123456789","webhookSource":"async"}.2026-03-05T14:30:01.1234567+00:00
  1. Compute the HMAC-SHA256 of the signed message using your integrator-specific secret (provided during webhook configuration) as the key. Both the message and key are UTF-8 encoded.

  2. The resulting signature is a lowercase hexadecimal string (64 characters).

Verification Steps

To verify an incoming webhook:

  1. Extract the X-Halo-Signature-256 header value from the request.
  2. Read the raw request body as a UTF-8 string (do not re-serialize or reformat it).
  3. Extract the X-Halo-Timestamp header value.
  4. Construct the signed message: <raw_body>.<X-Halo-Timestamp>.
  5. Compute HMAC-SHA256 using your secret and convert the result to a lowercase hex string.
  6. Compare your computed signature with the received X-Halo-Signature-256 value using a constant-time comparison to prevent timing attacks.
  7. If the signatures do not match, reject the request with 401 Unauthorized.

Replay Prevention

The X-Halo-Timestamp header is included in the signed message, binding the signature to a specific point in time. To guard against replay attacks, you should:

  1. Parse the X-Halo-Timestamp value as a timezone-aware datetime (e.g. DateTimeOffset in .NET).
  2. Reject requests where the timestamp is more than 5 minutes old (or a tolerance appropriate for your use case).

Example: Verifying in C#

using System.Security.Cryptography;
using System.Text;

public static bool VerifyWebhookSignature(string requestBody, string timestamp,
    string receivedSignature, string secret)
{
    var signedMessage = $"{requestBody}.{timestamp}";
    var messageBytes = Encoding.UTF8.GetBytes(signedMessage);
    var secretBytes = Encoding.UTF8.GetBytes(secret);

    using var hmac = new HMACSHA256(secretBytes);
    var hashBytes = hmac.ComputeHash(messageBytes);
    var expectedSignature = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();

    return CryptographicOperations.FixedTimeEquals(
        Convert.FromHexString(expectedSignature),
        Convert.FromHexString(receivedSignature));
}

Example: Verifying in Python

import hmac
import hashlib

def verify_webhook_signature(request_body: str, timestamp: str,
                              received_signature: str, secret: str) -> bool:
    signed_message = f"{request_body}.{timestamp}"
    expected_signature = hmac.new(
        secret.encode("utf-8"),
        signed_message.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected_signature, received_signature)

Example: Verifying in Node.js

const crypto = require("crypto");

function verifyWebhookSignature(requestBody, timestamp, receivedSignature, secret) {
  const signedMessage = `${requestBody}.${timestamp}`;
  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(signedMessage, "utf8")
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, "hex"),
    Buffer.from(receivedSignature, "hex")
  );
}

URL Configuration

To configure webhook notifications for your subscription, contact Support. You will need to provide the HTTPS URL you want webhooks sent to.

Separate URLs can be configured for async and registered webhooks. If only one URL is provided, it will be used for both.

Your current webhook URL configuration can be viewed in the Halo Portal:

Production Stage
manage.haloconnect.io manage.stage.haloconnect.io

URL Template Variables

The following optional template variables can be used in your webhook URL:

Variable Replaced With
{siteId} The site ID from the query (UUID).
{queryId} The query ID from the query (UUID).
{source} The source of the query: Registered or Async.

These values are replaced when the webhook is sent. The same values are also included in the payload body.

Example: If your configured URL is:

https://myintegration.com/webhooks/{siteId}/{source}

For a query with site ID 12345678-abcd-ef01-2345-6789abcdef01 and source async, the webhook will be sent to:

https://myintegration.com/webhooks/12345678-abcd-ef01-2345-6789abcdef01/Async

Warning

URL template variable replacement is case-sensitive. The {source} variable is replaced with the PascalCase enum name (Registered or Async), not the lowercase value used in the JSON payload (registered or async).

Retry Behaviour

Webhooks automatically retry up to 10 times to ensure reliable delivery, using a decorrelated jitter exponential backoff strategy:

  • First retry: approximately 5 seconds after the initial attempt.
  • Maximum delay between retries: 90 minutes.
  • Total retry window: all retries are spread across approximately 90 minutes from the initial attempt.
  • Strategy: decorrelated jitter backoff, which adds randomisation to prevent thundering herd problems.

If all 10 retries are exhausted, the webhook is permanently failed and no further delivery attempts are made.

HTTP Timeout

Each webhook delivery attempt has a 5-second timeout. If the integrator's endpoint does not respond within 5 seconds, the attempt is treated as a transient failure and retried.

Integration Guide

Respond Immediately

  • Return 200 OK within a few seconds. Our per-attempt timeout is 5 seconds.
  • If we do not receive a 2xx response, we retry with exponential backoff over approximately 90 minutes (up to 10 retries).
  • Avoid performing business logic, database writes, or external calls before responding.

Offload to a Durable Queue

  • Push the raw payload onto a durable queue (for example Azure Service Bus, RabbitMQ, or Amazon SQS).
  • Return 200 OK immediately after queuing.
  • Process the event asynchronously with a background worker.

Implement Idempotency

  • We may deliver the same event more than once due to timeouts or network issues.
  • Each payload includes a unique id field. Store processed IDs and skip duplicates.
  • Your processing logic should be safe to execute multiple times for the same event.

Validate and Secure

  • Verify the HMAC signature in the X-Halo-Signature-256 header (see HMAC Signature Verification).
  • Reject invalid signatures with 401 Unauthorized.
  • Use HTTPS only. Webhook URLs must use the https:// scheme.

Return Appropriate Status Codes

Status Code Behaviour
200–299 Success — no retry.
400 Permanent failure — no retry.
401 Permanent failure — no retry.
403 Permanent failure — no retry.
404 Permanent failure — no retry.
408 (Timeout) Transient — we will retry.
429 (Too Many Requests) Transient — we will retry.
5xx Transient — we will retry.

Tip

  • Avoid returning 5xx, 408, or 429 unless you genuinely want us to retry delivery.
  • Retries are intended for transient infrastructure issues (such as temporary unavailability), not business logic failures.
  • Business logic issues should return 200 and be handled on your side.