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.
visibilitytells 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-Deliveryfor idempotency and the payload'screated_atfor 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 thesecretever appears.
- Name
PATCH /webhooks/endpoints/:id- Type
- Description
Update
name,url,eventsoractive.
- 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 (
limitquery param, default 50, max 100).
- Name
POST /webhooks/endpoints/:id/test- Type
- Description
Synchronously send a
webhook.testevent and return{ success, statusCode, error }.
Request
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…"
}
