Gareth Wilson Gareth Wilson

Guide to Jobber Webhooks: Features and Best Practices

Published


Jobber webhooks tell your app when something changes in a Jobber account that has connected to it: a client is created, an invoice is updated, an account connects or disconnects. Jobber is field service management software for home service businesses, and if you build an app on its GraphQL API, webhooks replace polling for those changes.

This guide covers how Jobber webhooks are configured and signed, the thin payload, the very short response limit, and the practices that keep a Jobber receiver reliable.

What are Jobber webhooks?

Jobber webhooks belong to apps, not to individual Jobber accounts. You configure them in the Developer Center on your app's settings page, one topic and one URL per webhook. When the topic's event happens in any account connected to your app, Jobber sends an HTTP POST to the URL.

The payload says what happened, in which account, and to which object, but doesn't include the object. You use the IDs to query the GraphQL API.

Every request is signed. The X-Jobber-Hmac-SHA256 header is the base64 HMAC-SHA256 of the raw body, keyed with your app's OAuth client secret. There is no separate webhook secret.

Jobber webhook features

FeatureDetails
ConfigurationPer app, in the Developer Center
SubscriptionOne topic and URL per webhook
TopicsOBJECT_ACTION form, listed in the Developer Center and as WebHookTopicEnum in the schema
ScopesThe app needs the read scope for the topic's object
Payloaddata.webHookEvent with topic, appId, accountId, itemId, occurredAt
Content typeapplication/json, or form-encoded for apps created before 11 April 2022
Signature headerX-Jobber-Hmac-SHA256
SchemeHMAC-SHA256, base64, no prefix
Signed contentThe raw request body only
SecretThe app's OAuth client secret
Response window1 second
DeliveryAt least once

Jobber publishes no retry schedule and no source IP list, and the Developer Center doesn't alert you to failed responses.

Common events

Jobber calls its events topics. The Developer Center lists every topic when you add a webhook, and the same list is the WebHookTopicEnum type in the GraphQL schema. Topics Jobber names in its webhook docs include:

TopicSent when
CLIENT_CREATEA user creates a client. Needs the client read scope
INVOICE_UPDATEAn invoice changes, for example when a payment is applied or the status changes
APP_CONNECTAn account connects your app
APP_DISCONNECTAn account disconnects your app. Apps in the Jobber App Marketplace must handle it

Here is Jobber's example payload:

{
  "data": {
    "webHookEvent": {
      "topic": "CLIENT_CREATE",
      "appId": "3ef22a50-072d-430c-a78f-b7646657560b",
      "accountId": "MQ==",
      "itemId": "MQ==",
      "occurredAt": "2026-07-16T16:31:31-06:00"
    }
  }
}
  • topic is what happened. Route on it.
  • accountId is the Jobber account, which matters because one app serves many accounts.
  • itemId is the affected object. Query the API with it to get the details.
  • occurredAt is when it happened. Apps created before 8 December 2023 receive it misspelt as occuredAt.

Setting up Jobber webhooks

  1. In the Developer Center, open your app's settings page.
  2. Add a webhook: choose a topic and enter the URL.
  3. Make sure the app has the read scope for the topic's object, such as the client read scope for CLIENT_CREATE.
  4. Store your app's OAuth client secret server-side, for example as JOBBER_CLIENT_SECRET. It is the signing key.

If you use Hookdeck, create a source with the Jobber source type and paste your app's OAuth client secret into it, then use the Hookdeck source URL as the webhook URL. Since every webhook of the app is signed with the same secret, one Hookdeck source can receive all of the app's topics.

Securing Jobber webhooks

Read X-Jobber-Hmac-SHA256, compute the HMAC-SHA256 of the raw request body with the OAuth client secret as the key, encode it as base64, and compare the two strings in constant time.

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

function verifyJobberSignature(rawBody, signatureHeader, clientSecret) {
  if (!signatureHeader || !clientSecret) return false;
  const expected = Buffer.from(
    crypto.createHmac('sha256', clientSecret).update(rawBody).digest('base64'),
    'utf8'
  );
  const received = Buffer.from(signatureHeader.trim(), 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/jobber', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyJobberSignature(req.body, req.get('x-jobber-hmac-sha256'), process.env.JOBBER_CLIENT_SECRET)) {
    return res.sendStatus(401);
  }

  const { webHookEvent } = JSON.parse(req.body.toString('utf8')).data;
  // Queue webHookEvent and return at once; fetch the object by itemId in a background job.
  res.sendStatus(200);
});

The same check in Python:

import base64
import hashlib
import hmac


def verify_jobber_signature(raw_body: bytes, signature_header, client_secret: str) -> bool:
    if not signature_header or not client_secret:
        return False
    expected = base64.b64encode(hmac.new(client_secret.encode("utf-8"), raw_body, hashlib.sha256).digest())
    return hmac.compare_digest(signature_header.strip().encode("utf-8"), expected)


# In FastAPI: raw_body = await request.body()
# verify_jobber_signature(raw_body, request.headers.get("x-jobber-hmac-sha256"),
#                         os.environ["JOBBER_CLIENT_SECRET"])

Jobber's Node sample passes the two buffers straight to crypto.timingSafeEqual, which throws when their lengths differ. The length check above avoids that. For apps created before 11 April 2022, the body is form-encoded rather than JSON: verify the raw form body the same way, and parse it as a form afterwards. The signature covers only the body and there is no timestamp, so there is no replay window to check.

Make Jobber webhooks production-ready. Hookdeck Event Gateway verifies the X-Jobber-Hmac-SHA256 header on every delivery and answers Jobber within its one-second limit, then delivers each event to your app at a pace it can handle.

Jobber webhook limitations and pain points

One second to respond

The Problem: Your app's webhooks are disabled after a busy period.

Why It Happens: Jobber requires a response within 1 second. If response times consistently exceed that, or a large share of responses are errors, Jobber may disable the app's webhooks to protect other apps and systems. Cold starts, slow databases and inline API calls all push you past a second.

Workarounds:

  • Verify, enqueue and return 200 without any other work in the request.
  • Keep the receiver warm, and keep it separate from slower parts of your app.

How Hookdeck Can Help: Hookdeck answers Jobber straight away, so your own response time no longer counts, and then delivers to your app with the timeout and retries you configure.

Duplicate events by design

The Problem: The same topic arrives twice for the same object within a second or so.

Why It Happens: Delivery is at least once, so a network timeout can cause a resend. Separately, some actions fire the same topic more than once: adding a payment to an invoice fires INVOICE_UPDATE once for the payment and once for the status change. Each one is a separate event with its own payload.

Workarounds:

  • Fetch the current object from the API and upsert it, so processing it twice is harmless.
  • Collapse events with the same topic and itemId that arrive within a short window, as Jobber suggests.

How Hookdeck Can Help: Hookdeck's deduplication can drop events with the same topic and itemId within a time window you set.

Failures are silent

The Problem: Webhook failures go unnoticed until customers report missing data.

Why It Happens: The Developer Center doesn't notify you about unexpected responses, such as status codes outside the 2xx range. Jobber leaves monitoring to the app developer.

Workarounds:

  • Alert on non-2xx responses and latency from your webhook route.
  • Reconcile periodically through the API for important objects.

How Hookdeck Can Help: Hookdeck raises issues when deliveries to your app fail, and shows each request and response in its log.

Legacy app behaviour

The Problem: Code written from today's docs fails for an older app.

Why It Happens: Apps created before 11 April 2022 receive application/x-www-form-urlencoded bodies, and apps created before 8 December 2023 receive occuredAt instead of occurredAt.

Workarounds:

  • Check your app's creation date, and handle both spellings of the timestamp field.
  • Verify the raw body before parsing, whichever content type arrives.

How Hookdeck Can Help: Hookdeck's transformations can normalise older payloads, for example renaming occuredAt, before they reach your handler.

Best practices

Verify against the raw body

Jobber signs the raw payload. Use express.raw({ type: '*/*' }) in Express, await request.text() in Next.js and await request.body() in FastAPI, so the check works for JSON and form-encoded bodies alike.

Keep the client secret in one place

The OAuth client secret signs webhooks and authenticates your app. If you rotate it, the webhook signing key changes with it, so update your receiver at the same time.

Handle APP_DISCONNECT

Marketplace apps must handle APP_DISCONNECT. Use it to stop processing and clean up data for that account.

Acknowledge fast and process asynchronously

With a one-second limit, every handler needs this pattern. Verify, enqueue, return 200, and do everything else in a worker. See why to process webhooks asynchronously and our guide to webhook idempotency.

Conclusion

Jobber signs app webhooks with X-Jobber-Hmac-SHA256, a base64 HMAC-SHA256 of the raw body keyed with your app's OAuth client secret. Verify against the raw bytes, return within one second, expect duplicates from both retries and multi-step actions, and fetch objects by itemId in the background.

Hookdeck Event Gateway verifies every Jobber signature and responds inside the one-second limit, so slow processing on your side doesn't put your app's webhooks at risk.

Get started with Hookdeck for free and handle Jobber webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.