Webhooks
Register an HTTPS endpoint once:
curl https://app.quinable.com/api/partner/v1/webhook_endpoints \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "url":"https://scheduler.example.com/quinable/webhook", "enabled_events":["*"] }'
The response includes a signing secret (qwhsec_…) shown once. Quinable then POSTs each
event to your URL.
Event types: route.created, route.filled, route.reopened, route.canceled,
bid.placed, bid.accepted, bid.withdrawn, bid.rejected, bid.called_off, bid.ncns, bid.agency_canceled,
visit.clocked_in, visit.clocked_out, timesheet.approved, timesheet.rejected,
timesheet.paid, timesheet.updated, route.updated, webhook_endpoint.disabled.
Verify every delivery. Each request carries:
Quinable-Signature: t=1756150803,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e...
where v1 = HMAC-SHA256(t + "." + raw_request_body, your_endpoint_secret). This is Stripe's
scheme — reuse a Stripe verification snippet with the header name swapped. Node example:
const crypto = require('crypto');
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Rules of the road:
- Respond
2xxwithin 10 seconds. Non-2xx (or a timeout) is retried with backoff up to ~24h. - Deliveries are at-least-once and may arrive out of order — dedupe on the event
idand treatdata.objectas a snapshot, not a delta. - An endpoint that fails continuously is auto-disabled; catch up with
GET /events?created_after=<timestamp>.