Developer Center
Getting Started
Using Jobber’s API
Building Your App
Publishing Your App
App Template Project
Custom Integrations
Changelog

Setting up Webhooks

Webhooks allow your app to receive real-time notifications when events occur in a connected Jobber account, rather than polling the API for changes.

Webhooks are configured in the Developer Center per app, where a full list of available webhook events is also accessible. When a subscribed event occurs on a connected account, Jobber sends an HTTP POST request containing the webhook payload to the URL you have configured. For example, if you subscribe to CLIENT_CREATE, your endpoint receives a POST request each time a user creates a new client.

Configuration

Webhooks are managed from the app settings page in the Developer Center. Each webhook requires a topic (the event to listen for) and a URL (the endpoint Jobber sends the payload to). To receive webhook data for a given topic, your app must have the appropriate read scope for that object. For example, receiving CLIENT_CREATE events requires the client read scope.

Supported topics

All supported webhook topics are listed in the Developer Center when configuring a webhook for your app. They are also available as WebHookTopicEnum in the GraphQL schema. Topics follow the pattern OBJECT_ACTION.

APP_DISCONNECT handling is required for apps published in the Jobber App Marketplace. See Handling App Disconnects for details.

Webhook payload

Jobber sends the following JSON payload to your configured URL:

{
  "data": {
    "webHookEvent": {
      "topic": "CLIENT_CREATE",
      "appId": "3ef22a50-072d-430c-a78f-b7646657560b",
      "accountId": "MQ==",
      "itemId": "MQ==",
      "occurredAt": "2026-07-16T16:31:31-06:00"
    }
  }
}

The payload identifies what happened (topic), which account it happened on (accountId), and which object was affected (itemId). It does not include the full object data. To retrieve the object details, use the itemId to query the API. The webhook payload sent to the provided URL is introspectable in our GraphQL schema, see WebHookPayload for the most up-to-date payload, which is the result of the webHookEvent query.

ℹ️ For apps created before December 8, 2023: The timestamp field is named occuredAt (missing an 'r'). Apps created after this date use the corrected spelling occurredAt.

ℹ️ For apps created before April 11, 2022: The payload content type is application/x-www-form-urlencoded. Apps created after this date receive application/json.

Delivery

Webhook requests must be responded to within 1 second of receipt. To meet this requirement, process webhook payloads asynchronously. Your app should acknowledge receipt immediately and handle the event in a background job or queue. Your app must be resilient to sudden spikes in volume. If response times consistently exceed the 1-second limit, or if a large proportion of responses are errors requiring retries, Jobber may disable the app's webhooks to protect other apps and systems.

It is the responsibility of the app developer to monitor that all webhook URLs can successfully receive requests. The Developer Center does not currently send automated notifications for unexpected webhook responses (e.g. HTTP status codes outside the 2xx range).

At-least-once delivery

Jobber webhooks provide at-least-once delivery. In certain circumstances the same webhook may be delivered more than once. For example, if initial delivery appears to fail due to a network timeout, the webhook will be re-sent. Apps should detect duplicate deliveries based on the payload data and handle them idempotently.

Duplicate webhooks from a single action

Some user actions in Jobber trigger the same webhook topic more than once because the action affects multiple attributes on the object. These are not retries or delivery errors. They are separate webhook events fired in quick succession (typically about one second apart) because the underlying action triggers multiple updates. For example, adding a payment to an invoice fires INVOICE_UPDATE twice: once for the payment being applied and once for the invoice status changing. Your app should be designed to handle multiple webhook deliveries for the same object in quick succession. A common approach is to deduplicate by checking the itemId and topic within a short time window, or by making your webhook processing idempotent so that handling the same event twice produces the same result.

Verifying webhook authenticity

Before you respond to the webhook, you should verify that the webhook was sent from Jobber. A calculated signature is sent with every webhook, which you can use to verify the authenticity of the request.

Each request will include a base64 encoded X-Jobber-Hmac-SHA256 header, which is generated using your app's OAuth client secret and the data sent in the webhook.

Both examples below return a boolean indicating whether the webhook is authentic.

Ruby:

MY_SECRET = "my app's OAuth client secret"

# 'data' is the raw JSON payload
# 'hmac_header' is the base64 encoded header `X-Jobber-Hmac-SHA256`
def verify_webhook(data, hmac_header)
  digest = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), MY_SECRET, data)
  calculated_hmac = Base64.strict_encode64(digest)

  # https://api.rubyonrails.org/classes/ActiveSupport/SecurityUtils.html
  # Compare strings of variable length, without exposing yourself to a timing attack
  ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
end

verify_webhook(
  "{\"data\":{\"webHookEvent\":{\"topic\":\"APP_CONNECT\",\"appId\":\"id\",\"accountId\":\"id\",\"itemId\":\"id\",\"occurredAt\":\"ISO8601DateTime\"}}}",
  "ks1dre6TCHsMO2GVWnDYmx3ZrxubXGbCNZ5gPiXvP9E="
)
# => returns boolean value which determines authenticity

Node.js:

const crypto = require("crypto");
const MY_SECRET = "your app's OAuth client secret";
// 'data' is the raw JSON payload
// 'hmacHeader' is the base64 encoded header `X-Jobber-Hmac-SHA256`
function verifyWebhook(data, hmacHeader) {
  const digest = crypto
    .createHmac("sha256", MY_SECRET)
    .update(data)
    .digest("base64");
  // Compare signatures in constant time to protect against timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(digest),
    Buffer.from(hmacHeader)
  );
}
verifyWebhook(
'{"data":{"webHookEvent":{"topic":"APP_CONNECT","appId":"id","accountId":"id","itemId":"id","occurredAt":"ISO8601DateTime"}}}',
  "ks1dre6TCHsMO2GVWnDYmx3ZrxubXGbCNZ5gPiXvP9E="
);
// => returns boolean value which determines authenticity