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 */ }
} | Field | Meaning |
|---|---|
id | Unique delivery id (UUID). Use it for idempotency — the same event is never assigned a new id on retry. |
type | The event type, e.g. sample.created. Also sent as the X-Aliquora-Event header. |
createdAt | ISO-8601 timestamp of when the event was generated. |
organizationId | Your organization's numeric id. |
data | Event-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… | Header | Meaning |
|---|---|
X-Aliquora-Event | The event type (same value as type in the body). |
X-Aliquora-Delivery | The delivery/idempotency id (same value as id in the body). |
X-Aliquora-Timestamp | Unix time (seconds) when the request was signed. Part of the signed string. |
X-Aliquora-Signature | HMAC-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.
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).
| Event | Fires when | Source |
|---|---|---|
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