Skip to content
All documents

Developers

Webhooks reference

Aliquora

aliquora.com

Subscribe an HTTPS endpoint to lab lifecycle events and Aliquora will POST a signed JSON payload each time something happens — a sample is created, a result is verified, a COA is generated, and more. Every event, payload, and delivery rule below reflects the live implementation.

Document type

Webhooks reference

Audience

Developers & integrators

Delivery

POST · JSON · HMAC-SHA256 signed

Overview

Webhooks let your systems react to changes in Aliquora in real time, instead of polling the REST API. You register one or more HTTPS endpoints, choose which events each should receive, and Aliquora delivers a signed POST with a JSON body every time a subscribed event occurs. Every endpoint is scoped to your organization — you only ever receive events for your own tenant's data.

Endpoints are managed in the app by an administrator under Settings → Webhooks. When you create an endpoint you receive a signing secret (prefixed whsec_) exactly once — store it securely; it is used to verify that each delivery genuinely came from Aliquora. You can send a test event, rotate the secret, and inspect recent deliveries from the same screen.

Quick start. (1) In the app, go to Settings → Webhooks and add your HTTPS endpoint. (2) Select the events you want and copy the whsec_ secret. (3) Verify the X-Aliquora-Signature on each request (see below), respond 2xx, and process event.data.

The delivery envelope

Every delivery — regardless of event type — has the same top-level envelope. The event-specific fields live under data. The envelope id is the stable delivery/idempotency id, and it is also sent in the X-Aliquora-Delivery header.

{
  "id": "9f1c2e7a-3b4d-4c5e-8a6f-1d2e3f4a5b6c",
  "type": "sample.created",
  "createdAt": "2026-07-05T14:03:22.114Z",
  "organizationId": 42,
  "data": { /* event-specific — see below */ }
}
FieldMeaning
idUnique delivery id (UUID). Use it for idempotency — the same event is never assigned a new id on retry.
typeThe event type, e.g. sample.created. Also sent as the X-Aliquora-Event header.
createdAtISO-8601 timestamp of when the event was generated.
organizationIdYour organization's numeric id.
dataEvent-specific payload (documented per event below).

Request headers

Each delivery is a POST with Content-Type: application/json and these headers:

POST /your/webhook/path HTTP/1.1
Host: your-endpoint.example.com
Content-Type: application/json
User-Agent: Aliquora-Webhooks/1.0
X-Aliquora-Event: sample.created
X-Aliquora-Delivery: 9f1c2e7a-3b4d-4c5e-8a6f-1d2e3f4a5b6c
X-Aliquora-Timestamp: 1783605802
X-Aliquora-Signature: sha256=4e9d…
HeaderMeaning
X-Aliquora-EventThe event type (same value as type in the body).
X-Aliquora-DeliveryThe delivery/idempotency id (same value as id in the body).
X-Aliquora-TimestampUnix time (seconds) when the request was signed. Part of the signed string.
X-Aliquora-SignatureHMAC-SHA256 signature, formatted sha256=<hex>. See below.

Verifying the signature

Every request is signed with your endpoint's whsec_ secret so you can confirm it really came from Aliquora and was not tampered with in transit. The signature is an HMAC-SHA256, keyed by your secret, computed over the string:

${X-Aliquora-Timestamp}.${raw request body}

That is: the timestamp header, a literal ., then the exact raw bytes of the request body. The result is hex-encoded and sent as X-Aliquora-Signature: sha256=<hex>. To verify, recompute the same HMAC over the raw body you received and compare it to the header using a constant-time comparison. Sign the raw bytes — verify before any JSON parsing, because re-serializing can change whitespace and break the signature.

import crypto from "crypto";
import express from "express";

const app = express();

// Capture the RAW request body — you must sign the exact bytes we sent,
// so verify BEFORE any JSON parsing re-serializes the payload.
app.post(
  "/webhooks/aliquora",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const secret = process.env.ALIQUORA_WEBHOOK_SECRET; // "whsec_…"
    const rawBody = req.body.toString("utf8");
    const timestamp = req.header("X-Aliquora-Timestamp") ?? "";
    const received = req.header("X-Aliquora-Signature") ?? "";

    const expected =
      "sha256=" +
      crypto
        .createHmac("sha256", secret)
        .update(`${timestamp}.${rawBody}`)
        .digest("hex");

    const ok =
      received.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));

    if (!ok) return res.status(401).send("bad signature");

    const event = JSON.parse(rawBody);
    // Idempotency: skip if you've already processed event.id
    // (also available as the X-Aliquora-Delivery header).
    // ... handle event.type / event.data ...

    res.status(200).send("ok"); // respond 2xx to acknowledge
  },
);

Optionally, reject requests whose X-Aliquora-Timestamp is too old (for example, more than a few minutes) to limit replay of intercepted deliveries.

Try it: signature calculator

Paste a secret, timestamp, and raw body below to see the exact sha256=<hex> Aliquora would send. Use it to confirm your own verification code produces the same value. This runs entirely in your browser — nothing you type here is ever sent anywhere. It is a learning and testing aid only; use a throwaway secret, not a real production whsec_ key.

Signed string
X-Aliquora-Signature sha256=…

Idempotency

Delivery is at-least-once: a delivery that fails or times out will be retried, and in rare cases a request that actually reached you (but whose response we didn't receive) may be re-sent. Treat the X-Aliquora-Delivery header (equal to the envelope id) as an idempotency key: record the ids you have processed and skip any you have already handled. The id is stable across all retries of the same event.

Retries & backoff

Aliquora expects a 2xx response to acknowledge a delivery. Any non-2xx status, a connection error, or a timeout (requests time out after 10 seconds) counts as a failed attempt. Redirects are not followed — point the endpoint at its final URL. Failed deliveries are retried with exponential backoff: roughly 1 minute after the first failure, then doubling each time, up to a cap of 6 hours between attempts, for up to 6 attempts per event.

Auto-disable after repeated failures

To protect both sides, an endpoint that accumulates 15 consecutive hard delivery failures is automatically disabled and stops receiving events. A successful delivery resets the failure counter. If an endpoint is disabled, you'll see the reason in Settings → Webhooks; fix the endpoint and re-enable it there. Keep your endpoint returning 2xx quickly (do heavy work asynchronously) to avoid tripping this.

Events

The full event catalog. Most events are emitted directly from the action that causes them; the release-lifecycle and holding-time events are derived by a periodic scan and fire only when a sample transitions into a new state (so you won't get duplicate notifications for an unchanged sample).

EventFires whenSource
sample.created A new sample is registered. Direct emit
sample.received A sample is marked received. Direct emit
result.entered A result value is entered (not yet verified). Direct emit
result.verified A result is verified and locked. Direct emit
sample.blocked A sample enters a blocked release state (hard blocker present). Lifecycle scan
sample.ready_for_review A sample's only remaining blockers are soft (unverified result / approval required). Lifecycle scan
sample.ready_to_report A sample clears all release blockers and is ready to report. Lifecycle scan
sample.reported A sample is marked reported. Direct emit
coa.generated A Certificate of Analysis is generated for a sample. Direct emit
qc.failed A QC batch evaluates to a failing status. Direct emit
holding_time.expiring A sample's holding-time status changes (e.g. approaching or past expiry). Lifecycle scan
investigation.opened An investigation (e.g. CAPA / OOS) is opened. Direct emit

sample.created

{
  "sampleId": 1041,
  "trackingId": "S-2026-001041",
  "sampleIdentifier": "WW-INTAKE-07",
  "clientName": "Acme Water District",
  "status": "pending"
}

sample.received

{
  "sampleId": 1041,
  "trackingId": "S-2026-001041",
  "status": "received"
}

result.entered

{
  "sampleTestId": 5001,
  "testDefinitionId": 12
}

result.verified

{
  "sampleTestId": 5001,
  "sampleId": 1041
}

sample.blocked / sample.ready_for_review / sample.ready_to_report

These three release-lifecycle events share the same payload shape. reasons lists the current release readiness findings for the sample (each with a category, a severity, and a human-readable message); it is empty for sample.ready_to_report.

{
  "sampleId": 1041,
  "trackingId": "S-2026-001041",
  "sampleIdentifier": "WW-INTAKE-07",
  "clientName": "Acme Water District",
  "status": "processing",
  "reasons": [
    {
      "category": "unverified_result",
      "severity": "blocking",
      "message": "2 results are not yet verified."
    }
  ]
}

sample.reported

trackingId is included when a single sample is reported; on bulk-report the payload carries only sampleId and status. Treat trackingId as optional and key off sampleId.

{
  "sampleId": 1041,
  "trackingId": "S-2026-001041",
  "status": "reported"
}

coa.generated

{
  "sampleId": 1041,
  "trackingId": "S-2026-001041",
  "sampleIdentifier": "WW-INTAKE-07"
}

qc.failed

{
  "qcBatchId": 88,
  "batchNumber": "QC-2026-0088",
  "qcStatus": "failed"
}

holding_time.expiring

Extends the sample-lifecycle payload with holding-time detail: holdingStatus, the expiresAt timestamp (may be null), and the affected testNames. Fires on a change in holding-time status.

{
  "sampleId": 1041,
  "trackingId": "S-2026-001041",
  "sampleIdentifier": "WW-INTAKE-07",
  "clientName": "Acme Water District",
  "status": "processing",
  "reasons": [ /* release readiness reasons, as above */ ],
  "holdingStatus": "expiring_soon",
  "expiresAt": "2026-07-06T09:00:00.000Z",
  "testNames": ["Nitrate as N", "Total Coliform"]
}

investigation.opened

{
  "investigationId": 27,
  "investigationNumber": "INV-2026-0027",
  "title": "Out-of-spec nitrate — intake basin",
  "kind": "oos",
  "sampleId": 1041
}

Building an integration? Add your endpoint under Settings → Webhooks, send a test event, and verify the signature end-to-end before going live. Need an event we don't emit yet, or help wiring up delivery? Get in touch.

This document describes Aliquora product capabilities as of the date provided and is offered for evaluation purposes. It is not legal or regulatory advice and is not a warranty of compliance. Each organization is responsible for validating and confirming that the system meets its own regulatory obligations. Generated by Aliquora · aliquora.com