Guide to HaloPSA Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | Configuration > Integrations > Webhooks > New |
| Webhook type | Standard webhook, unless you're setting up a pre-configured integration |
| Method | POST to send data out of Halo. GET is for webhooks that retrieve data |
| Format | JSON |
| Authentication | No Authentication, or Basic Authentication. No signature |
| Payload types | Small object with key fields, full object with linked objects, event information only, or a custom payload |
| Events | Halo notification events, with conditions. At least one per webhook |
| Batching | Optional. Turns the body into a JSON array |
| Delivery log | The webhook's Deliveries tab, with re-send from logs |
| Retries and timeout | Not documented |
| Source IPs | Not 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:
| Event | Fires when |
|---|---|
| New Ticket Logged | A ticket is created |
| Ticket Reassigned | A ticket is reassigned |
| Document deleted on a Ticket | An attachment is deleted from a ticket (v2.242 and later) |
| Document edited on a Ticket | An 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
- Choose a username and generate a long random password, for example with
openssl rand -hex 32. - In Halo, go to Configuration > Integrations > Webhooks and click New.
- Enter a Webhook Name and your HTTPS endpoint as the Payload URL.
- Set Webhook Type to Standard webhook and Method to POST.
- Set Authentication to Basic Authentication and enter the username and password.
- Choose a Payload type. Start with small objects unless you need everything.
- Add at least one event, with conditions if you need them.
- Leave Batching on "One delivery for each occurrence of an event" until your receiver handles arrays.
- 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
2xxquickly 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: Basicheader. - 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.