Overview
Webhooks allow you to build or set up integrations which subscribe to certain events on the platform. When one of those events is triggered, we’ll send a HTTP POST payload to the webhook’s configured URL.
Event Types
The following events are available for subscription:
Appointments
APPOINTMENT:created: Triggered when a new appointment is created.APPOINTMENT:updated: Triggered when an appointment is updated.APPOINTMENT:completed: Triggered when an appointment is marked as completed.APPOINTMENT:cancelled: Triggered when an appointment is cancelled.APPOINTMENT:scheduled: Triggered when an appointment is scheduled.APPOINTMENT:status.appointment: Triggered when the appointment status changes.APPOINTMENT:status.invoice: Triggered when the invoice status of an appointment changes (This relates to the fieldworker, agent or client invoices, not the customer invoice)APPOINTMENT:submission: Triggered when a submission related to the appointment changes.
Jobs
JOB:created: Triggered when a new job is created.JOB:updated: Triggered when a job is updated.JOB:status.job: Triggered when the job status changes.JOB:status.quote: Triggered when the quotation status of a job changes.JOB:payment.received: Triggered when a payment is received for a job.JOB:payment.refunded: Triggered when a payment is refunded for a job.JOB:sales_pipeline_stage.updated: Triggered when the sales pipeline stage of a job is changed.
Customers
CUSTOMER:created: Triggered when a new customer is created.CUSTOMER:updated: Triggered when a customer is updated.
Invoices (fieldworker and agent invoices)
INVOICE:created: Triggered when a new agent or fieldworker invoice is created.INVOICE:updated: Triggered when an agent or fieldworker invoice is deleted.
Submission Batches
SUBMISSION_BATCH:created: Triggered when a new submission batch is created.SUBMISSION_BATCH:deleted: Triggered when a submission batch is deleted.
Payload Structure
Webhook payloads are sent as JSON in the body of a POST request. The data property contains specific details about the event, while the _links property provides HATEOAS links to related resources in our API, allowing you to easily fetch more information about the affected entity.
Example Payload
{
"event": "APPOINTMENT:updated",
"webhook_id": 123,
"occurred_at": "2026-05-04T12:14:00+00:00",
"entity": {
"type": "appointment",
"id": 45678
},
"data": {
"action": "updated",
"jobId": 1001,
"appointmentId": 45678,
"context": ["tags","questions"],
"_links": {
"self": "https://api.example.com/v1/appointment/45678",
"tags": "https://api.example.com/v1/appointment/45678/tags",
"questions": "https://api.example.com/v1/appointment/45678/questions"
}
}
}
Fields
| Field | Type | Description |
|---|---|---|
event |
string | The event name that triggered the webhook. |
webhook_id |
integer | The unique identifier for the webhook configuration. |
occurred_at |
string (ISO 8601) | The timestamp when the event occurred. |
entity.type |
string | The type of entity (e.g., appointment, job, customer). |
entity.id |
integer | The unique identifier for the entity. |
data |
object | Additional context or data related to the event. |
data._links |
object | HATEOAS links to related resources in the API. |
Security and Verification
If a Signing Secret is configured for the webhook, each request will include a signature header that allows you to verify that the request was sent by us and has not been tampered with.
Headers
X-Webhook-Timestamp: The Unix timestamp in milliseconds when the request was sent.X-Webhook-Signature: The HMAC SHA256 signature of the raw request body. Format:sha256=<hex_digest>.
Verifying the Signature
To verify the signature, you should:
- Retrieve the raw request body.
- Calculate the HMAC SHA256 hash of the raw body using your Signing Secret.
- Compare your calculated hash with the value provided in the
X-Webhook-Signatureheader (after removing thesha256=prefix).
PHP Example
$secret = 'your_signing_secret';
$signatureHeader = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
// 1. Check if signature exists
if (empty($signatureHeader)) {
die('Missing signature');
}
// 2. Extract signature (removing the sha256= prefix)
$receivedSignature = str_replace('sha256=', '', $signatureHeader);
// 3. Get raw payload
$payload = file_get_contents('php://input');
// 4. Calculate signature
$expectedSignature = hash_hmac('sha256', $payload, $secret);
// 5. Compare signatures securely
if (hash_equals($expectedSignature, $receivedSignature)) {
// Signature is valid
http_response_code(200);
} else {
// Signature is invalid
http_response_code(401);
die('Invalid signature');
}
Node.js Example
const crypto = require('crypto');
const secret = 'your_signing_secret';
const signatureHeader = request.headers['x-webhook-signature'];
const payload = JSON.stringify(request.body); // Use raw body if possible
const receivedSignature = signatureHeader.replace('sha256=', '');
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(receivedSignature))) {
// Signature is valid
} else {
// Signature is invalid
}