The Handyman Webhook Integration delivers real-time notifications to your HTTP endpoints. When data changes occur in Handyman (orders, installations, employee appointments, or reports), the system sends an HTTP POST with a JSON payload to your configured endpoint URLs. This guide explains how to receive, parse, and verify webhook deliveries.
Important: Filtering and Avoiding Infinite Loops
Currently, webhook notifications are sent only for changes that originate from Handyman Office or Handyman Mobile. In the future, webhook events will also be sent for changes that originate from the Handyman API. If your integration both consumes webhooks and writes data back through the Handyman API, implement filtering to ignore events triggered by your own integration. When API-originated webhooks are enabled, failing to filter may cause infinite loops (receive webhook → write via API → triggers new webhook → …). To implement filtering:
Implement this now so your integration is ready when API-originated webhooks roll out. |
Available Webhook Event Types?
Event Type |
Trigger |
Description |
|---|---|---|
Order |
An order or related data is created or modified |
Includes the IDs of affected orders and the reason for the change |
Installation |
An installation record is created or modified |
Includes the IDs of affected installations |
EmployeeAppointment |
An employee appointment is created or modified |
Includes the IDs of affected employee appointments |
Report |
A report has been generated and is ready for download |
Includes a download URL and reference to the parent order |
How Webhooks Are Delivered?
HTTP Method and Headers
All webhooks are delivered as HTTP POST requests with the following headers:
Content-Type:
application/jsonX-Webhook-Signature:
<base64-encoded signature>(present if HMAC verification is configured; see Verifying Sender Identity)
Expected Response
Respond with HTTP 2xx to acknowledge receipt. Any 4xx, 5xx, or network error is a delivery failure.
Retry Behavior
If delivery fails, the system retries up to 3 times with exponential backoff.
Attempt |
Delay Before Retry |
|---|---|
1st retry |
2 seconds |
2nd retry |
4 seconds |
3rd retry |
8 seconds |
Automatic Endpoint Disabling
The system tracks consecutive delivery failures across messages. If an endpoint accumulates 5 consecutive failed deliveries (across separate webhook events, not just retries within one event), it is automatically disabled until manually re-enabled in configuration. A successful delivery resets the counter to zero.
Payload Structure
Order Events
Sent when one or more orders (or related data such as customers, materials, etc.) change.
{
"EntityType": "Order",
"Ids": [1234, 5678],
"Reason": "Material",
"ChangedBy": "some-user-id"
}
Fields
Field |
Type |
Always Present |
Description |
|---|---|---|---|
EntityType |
string |
Yes |
Always "Order" for order events |
Ids |
array of integers |
Yes |
Affected Handyman order IDs; may contain multiple IDs |
Reason |
string |
No |
Indicates what aspect of the order changed. See the table below for possible values. May be absent if the reason is not available. |
ChangedBy |
string |
No |
Identifier of the user or integration that triggered the change. This field may not be present in every delivery. |
Possible values for Reason
Reason |
Description |
|---|---|
Order |
General order change (catch-all) |
Customer |
Customer associated with the order was modified |
Participant |
Participant added, removed, or modified |
Installation |
Installation linked to the order was modified |
Material |
Materials/line items added, removed, or modified |
Description |
Order description or notes modified |
SalaryCodeRegistration |
A salary code registration on the order was modified |
|
Important note on the Reason field: The Reason field is provided on a best-effort basis. How you should interpret it depends on the value:
In summary: a specific reason lets you narrow your processing scope; the Order reason means "something changed — re-process everything." |
Installation Events
Sent when one or more installation records change.
{
"EntityType": "Installation",
"Ids": [789, 101],
"ChangedBy": "some-user-id"
}Fields
Field |
Type |
Always Present |
Description |
|---|---|---|---|
EntityType |
string |
Yes |
Always "Installation" for installation events |
Ids |
array of integers |
Yes |
Affected Handyman installation IDs |
ChangedBy |
string |
No |
Identifier of the user or integration that triggered the change. This field may not be present in every delivery. |
Employee Appointment Events
Sent when one or more employee appointments change.
{
"EntityType": "EmployeeAppointment",
"Ids": [202, 303],
"ChangedBy": "some-user-id"
}
Fields
Field |
Type |
Always Present |
Description |
|---|---|---|---|
EntityType |
string |
Yes |
Always "EmployeeAppointment" for appointment events |
Ids |
array of integers |
Yes |
Affected Handyman employee appointment IDs |
ChangedBy |
string |
No |
Identifier of the user or integration that triggered the change. This field may not be present in every delivery. |
Report Events
Sent when a report has been generated and is ready for download.
{
"EntityType": "Report",
"ReportId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"OrderIdentifier": {
"Id": "1234",
"ExternalId": "EXT-5678"
},
"SasUrl": "https://storage.blob.core.windows.net/reports/...",
"ChangedBy": "some-user-id"
}
Fields
Field |
Type |
Always Present |
Description |
|---|---|---|---|
EntityType |
string |
Yes |
Always "Report" for report events |
ReportId |
string |
Yes |
Unique identifier for the generated report |
OrderIdentifier |
object |
Yes |
Reference to the order that the report belongs to |
OrderIdentifier.Id |
string |
Yes |
The Handyman order ID |
OrderIdentifier.ExternalId |
string |
Yes |
External/customer-facing ID for the order (may be null) |
SasUrl |
string |
Yes |
Pre-signed URL to download the generated report file (time-limited) |
ChangedBy |
string |
No |
Identifier of the user or integration that triggered report generation. This field may not be present in every delivery. |
Verifying Sender Identity (HMAC Signature)?
If an HMAC secret is configured, each webhook includes X-Webhook-Signature with a Base64-encoded HMAC-SHA256 of the raw request body. Verify to ensure authenticity and integrity.
How the Signature Is Computed
Raw JSON request body is taken as UTF-8 string.
Compute HMAC-SHA256 using your shared secret as key.
Base64-encode the hash bytes.
Send the Base64 string in the
X-Webhook-Signatureheader.
How to Verify (Step by Step)
Read the raw request body as a UTF-8 string; do not re-serialize before verifying.
Compute HMAC-SHA256 of the raw body with your secret.
Base64-encode the hash.
Compare to
X-Webhook-Signatureusing a constant-time comparison.If it matches, accept; otherwise, reject.
Code Examples
C#
using System.Security.Cryptography;
using System.Text;
public bool VerifyWebhookSignature(string rawRequestBody, string headerSignature, string secret)
{
var secretBytes = Encoding.UTF8.GetBytes(secret);
var payloadBytes = Encoding.UTF8.GetBytes(rawRequestBody);
using var hmac = new HMACSHA256(secretBytes);
var hashBytes = hmac.ComputeHash(payloadBytes);
var computedSignature = Convert.ToBase64String(hashBytes);
return computedSignature == headerSignature;
}Phyton
import hmac
import hashlib
import base64
def verify_webhook_signature(raw_body: bytes, header_signature: str, secret: str) -> bool:
secret_bytes = secret.encode('utf-8')
computed_hash = hmac.new(secret_bytes, raw_body, hashlib.sha256).digest()
computed_signature = base64.b64encode(computed_hash).decode('utf-8')
return hmac.compare_digest(computed_signature, header_signature)
JavaScript for Node.js
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, headerSignature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(rawBody, 'utf8');
const computedSignature = hmac.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(headerSignature)
);
}
Always use a constant-time comparison (e.g., timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing attacks. |
Best Practices for Consumers?
Filter out your own changes using
ChangedBy. Retrieve your identifier via GSG Handyman Onboarding to prevent infinite loops.Respond quickly with HTTP 2xx after persisting the event; do heavy processing asynchronously.
Handle duplicate deliveries; design processing to be idempotent.
Handle multiple IDs per delivery for Order, Installation, and EmployeeAppointment events.
Use the
Reasonfield for Order events to scope processing; ifOrder, re-process the entire order and children.Download reports promptly;
SasUrlis time-limited.Monitor endpoint health; 5 consecutive failures auto-disable the endpoint; ensure reliable 2xx responses.
Verify signatures in production when an HMAC secret is configured.
Quick Reference
Property |
Value |
|---|---|
HTTP method |
POST |
Content type |
application/json |
Signature header |
X-Webhook-Signature (HMAC-SHA256, Base64-encoded) |
Retry attempts |
3 (exponential backoff: 2s, 4s, 8s) |
Auto-disable threshold |
5 consecutive failed deliveries |
Expected response |
HTTP 2xx |
Current event sources |
Handyman Office, Handyman Mobile |
Planned future event sources |
Handyman API |