Webhooks allow you to build or set up integrations which subscribe to certain events on the platform.

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:

  1. Retrieve the raw request body.
  2. Calculate the HMAC SHA256 hash of the raw body using your Signing Secret.
  3. Compare your calculated hash with the value provided in the X-Webhook-Signature header (after removing the sha256= 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
}