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 LoopsCurrently, 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. |
What are the 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 |
PurchaseOrder | A purchase order has been received in Handyman (the purchase order itself, or its materials) | Includes the IDs of the received purchase orders. |
StoreMovements | Store movements intended for an ERP system are ready to be picked up | Includes the IDs of the affected store movements. Only ERP store movements are forwarded, and the receiver is responsible for deleting them once handled. |
| Inventory result | An inventory control for a store has been completed on Handyman Mobile | Includes the ID of the store. The receiver is responsible for getting the store content to send. |
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 you 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 cost element 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 you 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 you 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 you or integration that triggered report generation. This field may not be present in every delivery. |
Purchase Order Events
Sent when one or more purchase orders have been received in Handyman. A receipt on the purchase order itself or on its materials both produce this event.
{
"EntityType": "PurchaseOrder",
"Ids": [4321, 8765],
"ChangedBy": "some-user-id"
}Fields
Field | Type | Always Present | Description |
|---|---|---|---|
EntityType | string | Yes | Always "PurchaseOrder" for purchase order events |
Ids | array of integers | Yes | Affected Handyman purchase order IDs; may contain multiple IDs |
ChangedBy | string | No | Identifier of the user or integration that triggered the change. This field may not be present in every delivery. |
Reason field is sent for purchase order events. Fetch the purchase orders by ID from the Handyman API to see what was received.Required process: You must update purchase order Status and Number after processing the purchase order |
The receiver is required to update the status and number of the sent purchase order so that Handyman will see that the purchase order has been delviered to the recieving system.
|
Store Movement Events
Sent when store movements intended for an ERP system are ready to be picked up.
{
"EntityType": "StoreMovements",
"Ids": [55, 56, 57],
"ChangedBy": "some-user-id"
}Fields
Field | Type | Always Present | Description |
|---|---|---|---|
EntityType | string | Yes | Always "StoreMovements" for store movement events. Note the plural form. |
Ids | array of integers | Yes | Handyman store movement IDs; may contain multiple IDs. Only ERP store movements are ever included. |
ChangedBy | string | No | Identifier of the user or integration that triggered the change. This field may not be present in every delivery. |
Reason field is sent for store movement events. Fetch the store movements by ID from the Handyman API to read their contents.Required process: You must delete store movements after processing them |
Unlike every other event type, store movements are a work queue that the receiver is expected to drain:
|
Stocktaking result Events
Sent when Inventory/Stocktaking result is finalised on Handyman Mobile and approved in Handyman Office. The approval can be automatic (Normal case) or manual depending on store configuration in Handyman Office.
{
"EntityType": "StockTaking",
"Ids": [55,56],
"ChangedBy": "some-user-id"
}Fields
Field | Type | Always Present | Description |
|---|---|---|---|
EntityType | string | Yes | Always "StockTaking" for Inventory result Note the plural form. |
Ids | array of integers | Yes | Handyman store IDs. |
ChangedBy | string | No | Identifier of the user or integration that triggered the change. This field may not be present in every delivery. |
Reason field is sent for Inventory result events. Fetch the updated store content using Handyman Store ID from the Handyman API.Required process: You must update the SendStocktaking field after consuming the Inventory/Stocktaking result |
The receiver is required to update the status of the sent Store order so that Handyman will see that the Inventory Count has been received by the receiving system.
|
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. |
What is 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 |