Gareth Wilson Gareth Wilson

Guide to HaloPSA Webhooks: Features and Best Practices

Published


HaloPSA webhooks push service desk activity out of Halo: a ticket is logged or reassigned, or an attachment on a ticket changes, and Halo sends the details to a URL you choose. If you run an MSP or an internal IT team on HaloPSA, HaloITSM or HaloCRM, webhooks are how ticket events reach your billing, alerting, reporting or automation tools without polling the Halo API.

This guide covers how Halo webhooks are configured, how to authenticate them with Basic Authentication, the payload types and batching options, and the practices that make a Halo receiver dependable. Halo's documentation now lives on usehalo.com, and the halopsa.com guides redirect there. The guide applies to HaloPSA, HaloITSM and HaloCRM alike, because they share one webhook feature.

What are HaloPSA webhooks?

A Halo webhook is a profile with a payload URL, an authentication setting, a payload type and a list of events. Halo describes the events as "basically the same as Halo Notification profiles", for example New Ticket Logged or Ticket Reassigned, and you can put conditions on them as you would on a notification. When an event's conditions are met, Halo queues an HTTP POST to the payload URL with a JSON body, and records your endpoint's response on the webhook's Deliveries tab.

Halo doesn't sign webhooks. The authentication options are No Authentication and Basic Authentication, which takes a username and password. With Basic Authentication, your receiver checks the Authorization: Basic ... header against the pair you entered.

HaloPSA webhook features

FeatureDetails
ConfigurationConfiguration > Integrations > Webhooks > New
Webhook typeStandard webhook, unless you're setting up a pre-configured integration
MethodPOST to send data out of Halo. GET is for webhooks that retrieve data
FormatJSON
AuthenticationNo Authentication, or Basic Authentication. No signature
Payload typesSmall object with key fields, full object with linked objects, event information only, or a custom payload
EventsHalo notification events, with conditions. At least one per webhook
BatchingOptional. Turns the body into a JSON array
Delivery logThe webhook's Deliveries tab, with re-send from logs
Retries and timeoutNot documented
Source IPsNot published on the webhook guide

Common events

Halo doesn't publish a fixed list of webhook events. The events are the ones you'd use in a notification profile, chosen in the webhook's event list. Halo names these examples:

EventFires when
New Ticket LoggedA ticket is created
Ticket ReassignedA ticket is reassigned
Document deleted on a TicketAn attachment is deleted from a ticket (v2.242 and later)
Document edited on a TicketAn attachment on a ticket is edited (v2.242 and later)

Every payload type contains "an ID, notification ID, escmsg ID, event name, webhook ID, object ID, and timestamp". What else it carries depends on the payload type:

  • Small object with key fields only includes the basic properties of the entity.
  • Full object with linked objects includes everything relevant.
  • Event information only includes the object ID and event name.
  • Custom payload lets you choose the fields. For single-select fields, a setting chooses whether the database ID or the display value is sent.

Halo's example payload is a screenshot, so the guide doesn't give the JSON keys for those common properties as text. Send a test event and read the exact keys from your first delivery before you write routing code.

Setting up HaloPSA webhooks

  1. Choose a username and generate a long random password, for example with openssl rand -hex 32.
  2. In Halo, go to Configuration > Integrations > Webhooks and click New.
  3. Enter a Webhook Name and your HTTPS endpoint as the Payload URL.
  4. Set Webhook Type to Standard webhook and Method to POST.
  5. Set Authentication to Basic Authentication and enter the username and password.
  6. Choose a Payload type. Start with small objects unless you need everything.
  7. Add at least one event, with conditions if you need them.
  8. Leave Batching on "One delivery for each occurrence of an event" until your receiver handles arrays.
  9. Save, trigger the event, and check the Deliveries tab.

In Hookdeck. Create a source with the HaloPSA type, enter the same Username and Password, and use the source URL as the Payload URL. Choose Basic Authentication on the Halo webhook. The source accepts POST only, which matches Halo's outbound webhooks. Hookdeck never answers with a 401 challenge, so the credentials have to arrive with each request. Halo's guide doesn't say how it sends Basic credentials, so confirm on your first delivery that the request in Hookdeck carries an Authorization: Basic header. If you choose No Authentication in Halo, leave verification off on the source, though a credential is the better choice.

For local development, hookdeck listen 3000 halo-psa --path /webhooks/halo-psa with the Hookdeck CLI gives you a public HTTPS URL, with no account required.

Securing HaloPSA webhooks

Parse the Authorization header, decode the base64 credentials, split on the first colon, and compare the username and password in constant time. Evaluate both comparisons before deciding.

const crypto = require('crypto');
const express = require('express');

const app = express();

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on buffers of different lengths
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifyHaloRequest(header) {
  const user = process.env.HALO_WEBHOOK_USERNAME;
  const pass = process.env.HALO_WEBHOOK_PASSWORD;
  if (!user || !pass) return false; // fail closed when unconfigured

  const match = /^basic\s+([A-Za-z0-9+/]+={0,2})$/i.exec(header || '');
  if (!match) return false;
  const decoded = Buffer.from(match[1], 'base64').toString('utf8');
  const sep = decoded.indexOf(':'); // the password may contain colons
  if (sep === -1) return false;

  const userOk = safeEqual(decoded.slice(0, sep), user);
  const passOk = safeEqual(decoded.slice(sep + 1), pass);
  return userOk && passOk;
}

app.post('/webhooks/halo-psa', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyHaloRequest(req.get('authorization'))) return res.sendStatus(401);

  let body;
  try {
    body = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  const items = Array.isArray(body) ? body : [body]; // batching sends an array

  // Queue the items for a worker, then acknowledge.
  res.sendStatus(200);
});

The same check in Python:

import base64
import binascii
import hmac
import os
import re


def verify_halo_request(header) -> bool:
    user = os.environ.get("HALO_WEBHOOK_USERNAME")
    password = os.environ.get("HALO_WEBHOOK_PASSWORD")
    if not user or not password:
        return False  # fail closed
    match = re.match(r"^basic\s+([A-Za-z0-9+/]+={0,2})$", header or "", re.IGNORECASE)
    if not match:
        return False
    try:
        decoded = base64.b64decode(match.group(1), validate=True).decode("utf-8")
    except (binascii.Error, UnicodeDecodeError):
        return False
    if ":" not in decoded:
        return False
    got_user, got_password = decoded.split(":", 1)
    user_ok = hmac.compare_digest(got_user.encode("utf-8"), user.encode("utf-8"))
    password_ok = hmac.compare_digest(got_password.encode("utf-8"), password.encode("utf-8"))
    return user_ok and password_ok

Basic credentials prove the sender knows the pair. They don't prove the body is unchanged, and a captured request can be replayed, since there's no signature or timestamp. Use HTTPS, a long random password, and change it in Halo and your receiver together if it may have leaked.

Make HaloPSA webhooks production-ready. Hookdeck Event Gateway checks the Basic credentials on every Halo delivery, stores each event durably, and retries delivery to your service on a schedule you control.

HaloPSA webhook limitations and pain points

The payload keys aren't documented in text

The Problem: You can't write a parser for the common fields from the documentation alone.

Why It Happens: Halo's guide names the common properties in prose and shows the example payload and response only as screenshots. The event names are display labels, so check the exact string your instance sends.

Workarounds:

  • Trigger each event you care about and record the real payload before writing handlers.
  • Prefer small objects or a custom payload, so the body stays predictable.

How Hookdeck Can Help: Hookdeck stores every request with its headers and body, so your first test delivery becomes a reference payload you can search, compare and replay against your handler.

Batching changes the body's shape

The Problem: A receiver written for single objects breaks the day someone turns on batching.

Why It Happens: With batching on, "the webhook's payload will be a JSON array of objects rather than a single JSON object". Batching is only available for standard webhooks using POST and not with full-object payloads.

Workarounds:

  • Accept both shapes from the start: wrap a single object in an array.
  • Set Maximum items per batch so batches stay a size your receiver can process quickly.

How Hookdeck Can Help: Hookdeck transformations can normalise both shapes into one before delivery, so your service always sees the same structure.

Retries aren't documented

The Problem: You can't tell how long Halo keeps trying if your endpoint is down.

Why It Happens: Halo's guide covers the Deliveries tab and re-sending from logs, but not a retry schedule, a timeout or which status codes count as success.

Workarounds:

  • Respond with a 2xx quickly and do the work afterwards.
  • Watch the Deliveries tab, and re-send failed deliveries from the logs. This isn't available when Use lightweight logs is on, so leave it off unless payloads are very large.

How Hookdeck Can Help: Hookdeck accepts each Halo delivery as it arrives, then delivers it to your service with retries you configure, and issues alert you when your service starts failing.

Basic credentials and challenges

The Problem: A receiver that expects to challenge with a 401 and WWW-Authenticate before getting credentials never sees them.

Why It Happens: Halo's guide doesn't say whether Basic credentials are sent on the first request. Hookdeck doesn't send a challenge.

Workarounds:

  • Check the first delivery for an Authorization: Basic header.
  • Fail closed if it's missing rather than relying on a challenge.

How Hookdeck Can Help: Hookdeck's HaloPSA source checks the Basic credentials on each request. Hookdeck answers Halo with a 200 either way, then records a request without valid credentials as failed verification, so it never reaches your destination.

Best practices

Accept an object or an array

Normalise at the top of the handler so batching is a configuration change, not a code change.

Deduplicate on the delivery's IDs

Store the webhook's ID and object ID, together with the event and timestamp, and skip repeats. Re-sending from logs can deliver the same event twice. See our guide to webhook idempotency.

Use conditions to keep volume down

Filter events in Halo with the same conditions you'd use on notifications, rather than receiving every ticket change and discarding most of them.

Acknowledge fast and process asynchronously

Verify, store, respond with 200, then process. See why to process webhooks asynchronously.

Conclusion

HaloPSA sends a JSON POST for each configured notification event, or a JSON array when batching is on, with Basic Authentication as the only credential. Check the username and password in constant time over HTTPS, read the exact payload keys from a real delivery, accept both objects and arrays, and acknowledge quickly.

Hookdeck Event Gateway sits in front of your receiver, checks the Basic credentials, keeps every delivery, and retries delivery to your service, which fills the gap left by Halo's undocumented retry behaviour.

Get started with Hookdeck for free and handle HaloPSA webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.