Agent skill

HaloPSA Webhooks Skill

Receive and authenticate HaloPSA (also HaloITSM and HaloCRM) webhooks. Use when setting up a Halo webhook endpoint, checking the Basic Authentication header, debugging 401s, or handling notification events like New Ticket Logged and Ticket Reassigned.

Install this skill

npx skills add hookdeck/webhook-skills --skill halo-psa-webhooks


Halo (HaloPSA, HaloITSM, HaloCRM) sends a JSON webhook when a notification event fires, such as a ticket being logged or reassigned. Webhooks are not signed. The only authentication option is Basic Authentication: Halo sends Authorization: Basic base64(username:password) and your endpoint checks it against the username and password you entered on the webhook.

Choose Basic Authentication on the Halo webhook.

When to Use This Skill

  • How do I receive HaloPSA webhooks?
  • How do I authenticate Halo webhooks with Basic auth?
  • Does HaloPSA sign webhook payloads? (No: Basic Authentication is the only auth option)
  • How do I handle New Ticket Logged or Ticket Reassigned events?
  • How do I handle batched Halo webhooks (a JSON array)?
  • Why does my Halo webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

const digest = (v) => crypto.createHash('sha256').update(v).digest();
const safeEqual = (a, b) => crypto.timingSafeEqual(digest(a), digest(b)); // equal-length buffers

function verifyHaloPsaRequest(headers, env = process.env) {
  const m = /^basic +([A-Za-z0-9+/]+={0,2})$/i.exec(headers['authorization'] || '');
  if (!m || m[1].length % 4 || !env.HALO_PSA_WEBHOOK_USERNAME || !env.HALO_PSA_WEBHOOK_PASSWORD) return false;
  let decoded;
  try { decoded = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(m[1], 'base64')); }
  catch { return false; }
  const i = decoded.indexOf(':');                  // split on the FIRST colon
  if (i === -1) return false;
  const userOk = safeEqual(decoded.slice(0, i), env.HALO_PSA_WEBHOOK_USERNAME);
  const passOk = safeEqual(decoded.slice(i + 1), env.HALO_PSA_WEBHOOK_PASSWORD);
  return userOk && passOk;                         // both evaluated, no short-circuit
}
  • Header: Authorization: Basic base64(username:password). The scheme is case-insensitive. Accept only the strict base64 alphabet and decode strictly (validate=True in Python) so valid credentials followed by junk are rejected.
  • Compare the username and the password each in constant time (Node: SHA-256 digests or equal-length Buffers with crypto.timingSafeEqual; Python: hmac.compare_digest on bytes, never str, which raises on non-ASCII) and evaluate both before deciding.
  • Missing or malformed header, or a mismatch: 401. A body that isn't a JSON object (or array of objects): 400. Handled: 200.
  • The credentials must arrive on the first request. The Halo docs don't say whether Basic credentials are sent preemptively, and Hookdeck never sends a 401 + WWW-Authenticate challenge: it verifies the request as received.
  • A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS and rotate the credential if it leaks.
  • Read the body raw (express.raw({ type: '*/*' }), so any content type works) and authenticate before parsing it.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

Halo webhooks fire on the same events as Halo notification profiles. You pick at least one event per webhook, and can put conditions on them.

EventSent 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+)
Document edited on a TicketAn attachment is edited on a ticket (v2.242+)

Every payload carries an ID, notification ID, escmsg ID, event name, webhook ID, object ID and timestamp. The docs don't name those JSON properties in text, so the example payloads here are illustrative: inspect a real delivery. With batching on, the body is a JSON array of objects instead of one object. The Method setting also offers GET (for retrieving data), but notifications are POSTs and Hookdeck's HaloPSA source accepts POST only.

Environment Variables

HALO_PSA_WEBHOOK_USERNAME=halo-webhook                   # entered on the Halo webhook
HALO_PSA_WEBHOOK_PASSWORD=replace-with-a-long-random-password

Local Development

Halo can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 halo-psa --path /webhooks/halo-psa

Use the printed URL as the webhook's Payload URL in Halo. Hookdeck's source type is HALO_PSA, labelled "HaloPSA".

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →