# Jobber Webhooks

## When to Use This Skill

* How do I receive Jobber webhooks?
* How do I verify the `X-Jobber-Hmac-SHA256` header?
* Why is my Jobber webhook signature verification failing?
* How do I handle `CLIENT_CREATE`, `INVOICE_UPDATE`, `APP_CONNECT`, or `APP_DISCONNECT` webhooks?
* How do I fetch the changed object from a Jobber webhook payload?

Jobber webhooks are for **app developers**: they are configured per app in the Jobber Developer Center, not in an end-user Jobber account.

## Verification (core)

Jobber sends `X-Jobber-Hmac-SHA256`: the **base64** HMAC-SHA256 of the **raw** request body, keyed with your app's **OAuth client secret**. There is no timestamp or webhook ID in the signed content. Capture the raw body before JSON parsing, and compare the base64 strings in constant time (no base64 decoding of the header).

Node:

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

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(String(signatureHeader), 'utf8');
  if (received.length !== expected.length) return false; // timingSafeEqual throws otherwise
  return crypto.timingSafeEqual(received, expected);
}

```

Python:

```python
import hmac, hashlib, base64

def verify_jobber_signature(raw_body: bytes, signature_header: str | None, client_secret: str) -> bool:
    if not signature_header or not client_secret:
        return False
    expected = base64.b64encode(hmac.new(client_secret.encode(), raw_body, hashlib.sha256).digest())
    # Encode both sides to bytes: a non-ASCII header gives False (401), not a TypeError (500)
    return hmac.compare_digest(signature_header.encode("utf-8"), expected)

```

Jobber does not ship a webhook verification SDK, so verify manually. In Express, read the body with `express.raw({ type: '*/*' })` so any content type verifies.

> **For complete handlers with route wiring, event dispatch, and tests**, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/jobber-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/jobber-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/jobber-webhooks/examples/fastapi/)

## Payload Structure

Jobber payloads are thin: they say what happened, not the object itself. Query the Jobber GraphQL API with `itemId` to get the details.

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

```

Dispatch on `data.webHookEvent.topic`. Delivery is at-least-once and some actions fire the same topic twice in about a second, so handle duplicates idempotently. Respond within **1 second** and process asynchronously.

## Common Event Types

Topics follow the pattern `OBJECT_ACTION` and are values of `WebHookTopicEnum` in the Jobber GraphQL schema (the Developer Center lists all of them when you add a webhook).

| Topic | Triggered When |
| --- | --- |
| `CLIENT_CREATE` | A client is created (needs the client read scope) |
| `INVOICE_UPDATE` | An invoice changes, e.g. a payment is applied or the status changes |
| `APP_CONNECT` | An account connects your app |
| `APP_DISCONNECT` | An account disconnects your app (handling is required for Marketplace apps) |

> **For the full reference**, see [Jobber's webhook documentation](https://developer.getjobber.com/docs/using_jobbers_api/setting_up_webhooks/).

## Environment Variables

```bash
JOBBER_CLIENT_SECRET=your_jobber_oauth_client_secret   # Your app's OAuth client secret

```

## Hookdeck Setup

Jobber signs webhooks with your app's OAuth client secret. When you create a Jobber source in Hookdeck, paste that secret there so Hookdeck verifies `X-Jobber-Hmac-SHA256` before forwarding. Then set the Hookdeck source URL as the webhook URL for your app in the Jobber Developer Center. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/jobber-webhooks/references/setup.md).

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 jobber --path /webhooks/jobber

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/jobber-webhooks/references/overview.md) - Jobber webhook concepts, topics, payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/jobber-webhooks/references/setup.md) - Configure webhooks in the Developer Center, find the client secret, Hookdeck setup
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/jobber-webhooks/references/verification.md) - Signature verification details and gotchas