Webhooks

Webhooks let your app know when something happens on your tickets in Bob! Desk — without polling the API. Bob! Desk sends an HTTP POST request to your endpoint whenever a subscribed event occurs: a ticket changes status, or a quote, an invoice, a visit date or an intervention date is added. Endpoints are configured per company from the Bob! Desk dashboard under Settings → Webhooks (requires the API & Webhooks permission), or through the endpoint management API.

Event types

  • Name
    ticket.status_changed
    Type
    Description

    The ticket status changed (any transition: assigned, planned, finished, closed, canceled, …). The payload contains the previous and the new status.

  • Name
    ticket.quote_added
    Type
    Description

    A quote (devis) was added to the ticket.

  • Name
    ticket.invoice_added
    Type
    Description

    An invoice (facture) was added to the ticket.

  • Name
    ticket.visit_date_added
    Type
    Description

    A visit date (date de passage) was planned on the ticket.

  • Name
    ticket.intervention_date_added
    Type
    Description

    An intervention date was planned on the ticket.

  • Name
    ticket.document_added
    Type
    Description

    A document was added to the ticket's Documents tab. Only sent to companies allowed to view the document.

  • Name
    ticket.report_added
    Type
    Description

    An intervention report was added to the ticket.

  • Name
    ticket.message_added
    Type
    Description

    A message or note was added to the ticket. visibility tells whether it is shared with both parties (shared) or internal to your company (internal, managers).

  • Name
    webhook.test
    Type
    Description

    Sent when you click "Test" on an endpoint in the dashboard. Never sent otherwise.

Example payload — ticket.status_changed

{
  "event": "ticket.status_changed",
  "created_at": "2026-07-03T10:00:00.000Z",
  "data": {
    "ticket": {
      "id": "64ff2ab79ab7e72a4fb4ac20",
      "number": "1042",
      "title": "Fuite d'eau local technique"
    },
    "status": { "from": "waiting", "to": "assigned" }
  }
}

Example payload — ticket.quote_added

{
  "event": "ticket.quote_added",
  "created_at": "2026-07-03T10:00:00.000Z",
  "data": {
    "ticket": {
      "id": "64ff2ab79ab7e72a4fb4ac20",
      "number": "1042",
      "title": "Fuite d'eau local technique"
    },
    "quote": {
      "id": "64ff2ab79ab7e72a4fb4ac99",
      "reference": "DEV-2026-118",
      "amount": 480,
      "added_at": "2026-07-03T09:59:58.000Z"
    }
  }
}

Example payload — ticket.visit_date_added

{
  "event": "ticket.visit_date_added",
  "created_at": "2026-07-03T10:00:00.000Z",
  "data": {
    "ticket": {
      "id": "64ff2ab79ab7e72a4fb4ac20",
      "number": "1042",
      "title": "Fuite d'eau local technique"
    },
    "date": {
      "type": "visit",
      "date": "2026-07-10T08:00:00.000Z",
      "duration": 60
    }
  }
}

The data object always contains a ticket summary, plus one event-specific key: status ({ from, to }), quote / invoice ({ id, reference, amount, added_at }), date ({ type: "visit" | "intervention", date, duration }), document ({ id, name, mimetype, size, added_at }), report ({ id, name, added_at }), or message ({ text, visibility, author, created_at }).


Request headers

Every webhook request carries the following headers:

  • Name
    X-Bob-Event
    Type
    string
    Description

    The event type, e.g. ticket.status_changed.

  • Name
    X-Bob-Delivery
    Type
    string
    Description

    Unique id of this delivery. Retries of the same event keep the same id — use it for idempotency on your side.

  • Name
    X-Bob-Signature
    Type
    string
    Description

    Signature of the request body: t=<unix timestamp>,v1=<hex HMAC-SHA256>. See verifying signatures.


Verifying signatures

Each endpoint has a secret (whsec_…) shown once, when the endpoint is created. The signature is an HMAC-SHA256 of the string "<timestamp>.<raw request body>" computed with that secret. Always verify it before trusting a webhook, and reject stale timestamps to prevent replay attacks.

Verifying a request

const crypto = require('node:crypto')

function verifyBobSignature({ header, rawBody, secret, toleranceSeconds = 300 }) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}

// Express example — use the raw body, not the parsed JSON:
app.post('/webhooks/bob', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyBobSignature({
    header: req.headers['x-bob-signature'],
    rawBody: req.body.toString('utf8'),
    secret: process.env.BOB_WEBHOOK_SECRET
  })
  if (!ok) return res.status(401).end()
  res.status(200).end()
})

Keep your webhook secret safe. If it leaks, delete the endpoint and create a new one — secrets cannot be rotated in place or retrieved again.


Delivery and retries

  • Your endpoint must respond with a 2xx status code within 10 seconds. Anything else (including redirects, which are not followed) counts as a failure.
  • Failed deliveries are retried up to 5 times with increasing delays: 1 min, 5 min, 30 min, 2 h, 12 h.
  • After 10 consecutively exhausted deliveries, the endpoint is disabled automatically. Re-enable it from the dashboard once your endpoint is healthy — this resets the failure counter.
  • Delivery logs (status, HTTP code, attempts) are visible in the dashboard for 90 days.
  • Deliveries are at-least-once and ordering is not guaranteed: use X-Bob-Delivery for idempotency and the payload's created_at for ordering.

Endpoint URL requirements

  • HTTPS only.
  • Public hosts only: localhost, private and link-local IP ranges are rejected (also re-checked at delivery time after DNS resolution).
  • Maximum 10 endpoints per company.

Managing endpoints

Endpoints can also be managed programmatically on the TS API (https://jarvis.bob-desk.com), authenticated with the same cookie/JWT session as the dashboard and gated by the API & Webhooks permission. See the authentication page for details.

  • Name
    GET /webhooks/endpoints
    Type
    Description

    List the company's endpoints.

  • Name
    POST /webhooks/endpoints
    Type
    Description

    Create an endpoint (name, url, events[]). The response is the only place the secret ever appears.

  • Name
    PATCH /webhooks/endpoints/:id
    Type
    Description

    Update name, url, events or active.

  • Name
    DELETE /webhooks/endpoints/:id
    Type
    Description

    Delete (soft) an endpoint. No further events will be sent.

  • Name
    GET /webhooks/endpoints/:id/deliveries
    Type
    Description

    Last deliveries for this endpoint (limit query param, default 50, max 100).

  • Name
    POST /webhooks/endpoints/:id/test
    Type
    Description

    Synchronously send a webhook.test event and return { success, statusCode, error }.

Request

POST
/webhooks/endpoints
curl https://jarvis.bob-desk.com/webhooks/endpoints \
  -H "Cookie: session={cookie}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My integration",
    "url": "https://example.com/webhooks/bob",
    "events": ["ticket.status_changed", "ticket.invoice_added"]
  }'

Response — the secret is shown only here

{
  "id": "64b2e4d19ab7e72a4fb4ac20",
  "name": "My integration",
  "url": "https://example.com/webhooks/bob",
  "events": ["ticket.status_changed", "ticket.invoice_added"],
  "active": true,
  "consecutiveFailures": 0,
  "disabledReason": null,
  "createdAt": "2026-07-03T10:00:00.000Z",
  "updatedAt": "2026-07-03T10:00:00.000Z",
  "secret": "whsec_5f2c…"
}