# Guide to Simpro Webhooks: Features and Best Practices

Simpro webhooks tell your systems when work changes in a trade business's Simpro build: a job is created or moves stage, a quote is accepted, an invoice or payment is recorded, a customer's details change, or a schedule is updated. If you build integrations for trade and field-service businesses that run on Simpro, webhooks save you polling the API to find out what changed.

This guide covers how Simpro webhook subscriptions are created and signed, the events and their short notification payloads, what Simpro doesn't document, and the practices that keep a Simpro receiver reliable.

## What are Simpro webhooks?

A Simpro webhook subscription has a name, a callback URL, a Secret and a list of events. When a subscribed event happens, Simpro sends a JSON `POST` to the callback URL.

The body is a notification, not the record. It says what changed and gives the IDs you need to fetch the record from the Simpro API.

Each delivery carries an `X-Response-Signature` header: the hex HMAC-SHA1 of the raw request body, keyed with the Secret you entered on the subscription. You choose the Secret yourself. Simpro describes it as "a random string of your choice, used to verify that the message you received came from Simpro".

## Simpro webhook features

| Feature | Details |
| --- | --- |
| Who can create | Someone with administrative privileges in the Simpro build |
| Configuration | **System > Setup > API > Webhook Subscriptions**, or the Webhook Subscriptions API |
| Subscription | A list of events per subscription, and a status of `Enabled` or `Disabled` |
| Payload | `ID`, `build`, `description`, `name`, `action`, `reference`, `date_triggered` |
| Signature header | `X-Response-Signature` |
| Scheme | HMAC-SHA1, hex digest (40 characters), no prefix |
| Signed content | The raw request body only |
| Secret | A random string you choose and enter on the subscription |
| Testing | **Test Subscription** sends a `test.updated` body |
| Debugging | An optional email address receives debugging information |
| Retries, timeout and source IPs | Not published |

## Common events

The Webhook Subscriptions API lists 109 events, each named `<object>.<action>`:

| Area | Examples |
| --- | --- |
| Jobs | `job.created`, `job.updated`, `job.deleted`, `job.status`, `job.stage.pending`, `job.stage.progress`, `job.stage.complete`, `job.stage.invoiced`, `job.stage.archived` |
| Quotes and leads | `quote.created`, `quote.updated`, `quote.status`, `lead.created`, `lead.status` |
| Invoices and payments | `invoice.created`, `invoice.status`, `invoice.stage.approved`, `credit.created`, `payment.created`, `recurring.invoice.created` |
| Customers and contacts | `company.customer.created`, `company.customer.updated`, `individual.customer.created`, `contact.updated`, `customer.contract.created`, `customer.note.created` |
| Schedules | `job.schedule.created`, `quote.schedule.updated`, `lead.schedule.deleted`, `activity.schedule.created` |
| Sites and assets | `site.created`, `asset.updated`, `job.asset.tested`, `quote.asset.assigned` |
| Purchasing and stock | `purchase.order.created`, `purchase.order.receipt.created`, `stock.allocation.deleted` |
| People | `employee.created`, `employee.updated`, `employee.deleted` |

Notes and attachments have their own events on jobs, quotes, leads, invoices and sites, such as `job.note.created` and `job.attachment.updated`.

Here is Simpro's example for an updated company customer:

```json
{
  "ID": "company.customer.updated",
  "build": "{BUILD_URL}",
  "description": "Company customer #518 has been updated.",
  "name": "Company customer",
  "action": "updated",
  "reference": {
    "companyID": 0,
    "customerID": 518,
    "companyCustomerID": 518
  },
  "date_triggered": "2019-06-28T16:35:12+10:00"
}

```

* `ID` is the event name, not a record ID. Route on it.
* `build` is the URL of the Simpro build that sent the event, which matters if you integrate with several businesses.
* `reference` holds the IDs to pass to the API, such as `companyID` and `customerID`. Its keys depend on the event.
* `date_triggered` is ISO 8601 with the build's UTC offset.

## Setting up Simpro webhooks

Generate a long random Secret first, for example with `openssl rand -hex 32`, and store it server-side as `SIMPRO_WEBHOOK_SECRET`.

1. Ask an administrator of the Simpro build to open **System > Setup**, hover over **API** near the bottom and click **Webhook Subscriptions**. If **API** isn't there, the user isn't an administrator.
2. Click **Create Subscription**.
3. Enter a name and, optionally, an email address for debugging information.
4. Enter your Secret.
5. Enter your HTTPS endpoint as the **Callback URL**.
6. Select the events to subscribe to, and save.
7. Click **Test Subscription**. Simpro sends a `test.updated` body to your URL.

Through the Webhook Subscriptions API, the same subscription takes `Name`, `CallbackURL`, `Secret`, `Email`, `Description`, `Events` and `Status`. Only the name and callback URL are required, but always set `Secret`, because it is what lets you verify deliveries.

**With Hookdeck.** Create a source with the Simpro source type and paste the same Secret into **Webhook Signing Secret**, then use the Hookdeck source URL as the callback URL. A delivery whose `X-Response-Signature` doesn't match still gets a `200` from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

## Securing Simpro webhooks

Compute an HMAC-SHA1 of the raw request body with the Secret as a UTF-8 key, hex-encode it, and compare it with `X-Response-Signature` in constant time. Simpro's own PHP sample does exactly this with `hash_hmac('sha1', $requestBody, $secret)` and `hash_equals`.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifySimproSignature(rawBody, header, secret) {
  if (typeof header !== 'string' || !secret) return false;
  // Check the shape first: Buffer.from(x, 'hex') silently stops at invalid characters.
  if (!/^[0-9a-f]{40}$/i.test(header.trim())) return false;

  const expected = crypto.createHmac('sha1', secret).update(rawBody).digest();
  const received = Buffer.from(header.trim(), 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/simpro', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.SIMPRO_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifySimproSignature(req.body, req.get('x-response-signature'), secret)) {
    return res.sendStatus(401);
  }

  const notice = JSON.parse(req.body.toString('utf8'));
  // Route on notice.ID, then fetch the record from the API using notice.reference.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac

def verify_simpro_signature(raw_body: bytes, header, secret: str) -> bool:
    if not header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha1).hexdigest()
    # Compare bytes so a non-ASCII header fails instead of raising TypeError.
    return hmac.compare_digest(expected.encode("utf-8"), header.strip().lower().encode("utf-8"))

# In FastAPI: raw_body = await request.body()
# verify_simpro_signature(raw_body, request.headers.get("x-response-signature"),
#                         os.environ["SIMPRO_WEBHOOK_SECRET"])

```

To check your verifier, use the Secret `somerandomstring` (the value in Simpro's sample) and this exact body:

```
{"ID":"company.customer.updated","build":"https://example.simprosuite.com","description":"Company customer #518 has been updated.","name":"Company customer","action":"updated","reference":{"companyID":0,"customerID":518,"companyCustomerID":518},"date_triggered":"2019-06-28T16:35:12+10:00"}

```

The signature should be:

```
459ed50d5bcc1832729356005c71792d55e55d9d

```

HMAC-SHA1 is still sound as a message authentication code, and it is what Simpro sends, so there is nothing to choose here. What matters more is a long, random Secret.

> **Make Simpro webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `X-Response-Signature` header on every Simpro delivery, stores each job, quote and invoice event, and delivers it to your service with retries and replay you control.

## Simpro webhook limitations and pain points

### No timestamp means no replay protection

**The Problem:** A captured request can be sent again later, and it still verifies.

**Why It Happens:** Only the body is signed. `date_triggered` is inside the body, so it is covered by the signature, but Simpro doesn't say how old a delivery can be, and nothing stops the same body arriving twice.

**Workarounds:**

* Keep the endpoint on HTTPS.
* Make handlers idempotent, keyed on `ID`, the `reference` IDs and `date_triggered`.
* Because the body is only a notice, fetch the current record from the API before acting. A replayed notice then causes, at worst, a redundant read.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated deliveries with the same body before they reach your handler.

### Notifications, not data

**The Problem:** Every event needs at least one API call before you can do anything with it.

**Why It Happens:** Simpro sends the event name and the IDs in `reference`, not the record. The `reference` keys differ from event to event.

**Workarounds:**

* Map each `ID` to the API endpoint that fetches its record, using the keys in `reference`.
* Queue the fetch rather than doing it inside the webhook request.

**How Hookdeck Can Help:** Hookdeck can [limit the delivery rate](/docs/destinations#set-a-max-delivery-rate) to your service, which keeps your follow-up API calls to Simpro steady during bursts.

### Retries and failures aren't documented

**The Problem:** You don't know whether Simpro resends a notice that your endpoint missed.

**Why It Happens:** Simpro documents no timeout, retry schedule or success rule. The subscription's email address receives "debugging information", without further detail.

**Workarounds:**

* Set the debugging email on every subscription and watch it.
* Return a `2xx` quickly, and reconcile important records through the API after an outage.

**How Hookdeck Can Help:** Hookdeck accepts each delivery as soon as it arrives and stores it, then delivers to your service with [automatic retries](/docs/retries) and lets you replay anything that failed.

### The Secret is optional

**The Problem:** A subscription created through the API without a Secret can't be verified.

**Why It Happens:** The Webhook Subscriptions API requires only a name and a callback URL. Simpro doesn't document what a subscription without a Secret sends.

**Workarounds:**

* Always set a long random Secret, and reject requests without a valid `X-Response-Signature`.

**How Hookdeck Can Help:** Hookdeck's Simpro source requires the Secret, so a subscription that sends unsigned or wrongly signed requests shows up as failed verification in the request log.

## Best practices

### Verify against the raw body

The HMAC covers the exact bytes Simpro sent. Use `express.raw({ type: '*/*' })` in Express, `await request.text()` in a Next.js route handler and `await request.body()` in FastAPI, and parse only after the check passes.

### Handle `test.updated`

**Test Subscription** sends `"ID": "test.updated"`, which isn't a subscribable event. Acknowledge it with a `2xx` without processing it.

### Key data by build

If you integrate with several Simpro customers, use `build` and your stored subscription details to find the right tenant, and keep a separate Secret for each subscription.

### Acknowledge fast and process asynchronously

Verify, store the notice, return `200`, then fetch the record and do the work. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Simpro signs each webhook notice with a hex HMAC-SHA1 of the raw body in `X-Response-Signature`, keyed with a Secret you choose. Always set that Secret, verify against the raw bytes, route on `ID`, and fetch the full record using the IDs in `reference`.

[Hookdeck Event Gateway](https://hookdeck.com) verifies every Simpro signature, paces delivery to your service, and keeps each notice until it has been processed.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Simpro webhooks reliably in minutes.