QEHSQEHS

Developer portal

Webhooks

Push events to your systems as they happen in QEHS. Every delivery is signed with HMAC-SHA-256 and retried with exponential backoff for 24 hours.

Event catalog

EventDescriptionPayload shape
record.createdA new record is committed to a module.{ id, moduleKey, createdAt, createdBy, payload }
record.updatedAny field or workflow state changes on a record.{ id, moduleKey, changes[], updatedAt, updatedBy }
record.transitionedA workflow transition fires (including state name before/after).{ id, moduleKey, from, to, actor, at }
record.deletedA record is soft-deleted.{ id, moduleKey, deletedAt, deletedBy }
module.publishedA Composer version is published to production.{ moduleKey, version, publishedBy, publishedAt }
user.provisionedSCIM or self-sign-up created a new user.{ userId, email, provisionedAt, source }
user.deprovisionedA user is disabled or removed via SCIM.{ userId, at, source }
attachment.uploadedAn attachment is completed and virus-scanned.{ attachmentId, recordId, sizeBytes, mimeType }
export.completedAn asynchronous export job has a downloadable artefact.{ jobId, downloadUrl, expiresAt, kind }
billing.subscription.updatedPlan, seat count, or trial state changed (tenant-owner scope).{ tenantId, plan, seats, status, effectiveAt }

Signature verification

Every outbound delivery includes an X-QEHS-Signature header of the form t=<timestamp>,v1=<hex-hmac>. Re-compute the HMAC-SHA-256 of <timestamp>.<raw-body> using your endpoint's shared secret and compare with a constant-time equality check. Reject anything older than 5 minutes.

// Node.js / TypeScript
import crypto from 'node:crypto';

export function verify(body: string, header: string, secret: string): boolean {
  const [ts, sig] = header.split(',').map((p) => p.split('=')[1]);
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${body}`)
    .digest('hex');
  const match = crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'));
  const fresh = Math.abs(Date.now()/1000 - Number(ts)) < 300;
  return match && fresh;
}

Delivery semantics

  • At-least-once delivery. Use the X-QEHS-Delivery-Id header to dedupe.
  • 8 retry attempts over ~24 hours with exponential backoff (1m, 5m, 15m, 1h, 4h, 10h, 20h).
  • Any non-2xx response (or timeout > 15s) triggers a retry.
  • A delivery failing all retries moves to the dead-letter queue; admins receive an email.
  • Replay individual deliveries from https://app.qehsethos.com/settings/webhooks.

Setup

  1. Open https://app.qehsethos.com/settings/webhooks and click "Add endpoint".
  2. Enter the URL, pick events, optionally filter by module key.
  3. Copy the shared secret once — you will need it to verify signatures.
  4. Click "Send test" to trigger a sample delivery to your endpoint.