Quinable Partner APIv1.1.0Guide Clients Routes Bids Timesheets Events & Webhooks Lookups

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: