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.