Get started
Quickstart
This walks the whole loop with the sandbox token: order a panel, hear when the result is released, read the released report, and see what changed against the member's own baseline. Everything is synthetic. The examples assume the local demo on http://localhost:8000; on another host, change the base URL.
1. Place an order
Send your own reference for the person (any string up to 128 characters; a name or birth date is never accepted), their sex, age band and the panel. The response gives the member's Protearc id and the requisition code for the sample.
curl -X POST http://localhost:8000/v1/orders \
-H "Authorization: Bearer ptk_sandbox_meridian_demo" \
-H "Content-Type: application/json" \
-d '{"platform_ref": "meridian-user-1", "sex": "F", "age_band": "50-59", "panel": "brain"}'import requests
BASE = "http://localhost:8000"
HEADERS = {"Authorization": "Bearer ptk_sandbox_meridian_demo"}
order = requests.post(
f"{BASE}/v1/orders",
headers=HEADERS,
json={"platform_ref": "meridian-user-1", "sex": "F", "age_band": "50-59", "panel": "brain"},
).json()
print(order["member_id"], order["requisition_code"])const BASE = "http://localhost:8000";
const HEADERS = { Authorization: "Bearer ptk_sandbox_meridian_demo" };
const order = await (await fetch(`${BASE}/v1/orders`, {
method: "POST",
headers: { ...HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ platform_ref: "meridian-user-1", sex: "F", age_band: "50-59", panel: "brain" }),
})).json();
console.log(order.member_id, order.requisition_code);{
"member_id": "PA-482913",
"requisition_code": "RQ-3F9A1C20"
}2. Register a webhook
Tell Protearc where to send events. The response carries the signing secret, whsec_..., and it is shown only in this response, so keep it. The sandbox accepts http://localhost addresses; in every other case the address must be a public https:// URL. Registering the same address twice is refused with 409.
curl -X POST http://localhost:8000/v1/webhooks \
-H "Authorization: Bearer ptk_sandbox_meridian_demo" \
-H "Content-Type: application/json" \
-d '{"url": "http://localhost:9000/hooks"}'hook = requests.post(
f"{BASE}/v1/webhooks",
headers=HEADERS,
json={"url": "http://localhost:9000/hooks"},
).json()
SECRET = hook["secret"] # shown only in this response
print(hook["id"])const hook = await (await fetch(`${BASE}/v1/webhooks`, {
method: "POST",
headers: { ...HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ url: "http://localhost:9000/hooks" }),
})).json();
const SECRET = hook.secret; // shown only in this response
console.log(hook.id);{
"id": "5d0c8a1e-6b1f-4f6e-9b0e-0c9a4d7f2b11",
"url": "http://localhost:9000/hooks",
"created_at": "2026-11-04T16:20:00.000000Z",
"secret": "whsec_example_not_a_real_secret"
}3. Receive the event
When the laboratory releases a result, Protearc sends a signed POST to your address. This receiver checks the signature before it trusts the body, and answers 200 to acknowledge. Start it with the secret in PROTEARC_WEBHOOK_SECRET. The Webhooks page has the details of the headers, the signature and the retries.
import hashlib
import hmac
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
SECRET = os.environ["PROTEARC_WEBHOOK_SECRET"]
def verify(secret, timestamp, body, signature):
signed = timestamp.encode() + b"." + body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), signature.encode())
class Hook(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
if not verify(SECRET, self.headers.get("X-Protearc-Timestamp", ""), body,
self.headers.get("X-Protearc-Signature", "")):
self.send_response(401)
self.end_headers()
return
event = json.loads(body)
print(event["type"], event["data"]["member_ref"])
self.send_response(200)
self.end_headers()
if __name__ == "__main__":
HTTPServer(("", 9000), Hook).serve_forever()const crypto = require("node:crypto");
const http = require("node:http");
const SECRET = process.env.PROTEARC_WEBHOOK_SECRET;
function verify(secret, timestamp, body, signature) {
const expected = "sha256=" + crypto.createHmac("sha256", secret)
.update(`${timestamp}.`).update(body).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
http.createServer((req, res) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const body = Buffer.concat(chunks);
if (!verify(SECRET, req.headers["x-protearc-timestamp"] ?? "", body, req.headers["x-protearc-signature"] ?? "")) {
res.writeHead(401).end();
return;
}
const event = JSON.parse(body);
console.log(event.type, event.data.member_ref);
res.writeHead(200).end();
});
}).listen(9000);{
"created_at": "2026-11-04T16:31:08.512345+00:00",
"data": {
"draw_number": 4,
"member_ref": "PA-0001",
"report_id": "3b6f0c9e-52c4-4a43-8f27-7d1f0a6e9c10",
"status": "released"
},
"id": "9c1d7a52-0e0b-4a4e-a4f5-2f6f9d1b7c33",
"type": "result.released"
}4. Fetch the report
Use data.member_ref from the event. The report has one entry per released draw; values are NPQ (normalized protein quantification, log2 scale). Add ?format=fhir for a FHIR R4 Bundle of DiagnosticReport and Observation resources, with media type application/fhir+json. Only released results are ever returned. A member that is not yours, or does not exist, is 404.
curl http://localhost:8000/v1/members/PA-0001/reports \
-H "Authorization: Bearer ptk_sandbox_meridian_demo"
curl "http://localhost:8000/v1/members/PA-0001/reports?format=fhir" \
-H "Authorization: Bearer ptk_sandbox_meridian_demo"member_ref = "PA-0001" # from the event: event["data"]["member_ref"]
reports = requests.get(f"{BASE}/v1/members/{member_ref}/reports", headers=HEADERS).json()
latest = max(reports["reports"], key=lambda r: r["draw_number"])
print(latest["draw_number"], len(latest["results"]), "results")
bundle = requests.get(
f"{BASE}/v1/members/{member_ref}/reports",
headers=HEADERS,
params={"format": "fhir"},
).json()
print(bundle["resourceType"], len(bundle["entry"]), "entries")const memberRef = "PA-0001"; // from the event: event.data.member_ref
const reports = await (await fetch(`${BASE}/v1/members/${memberRef}/reports`, { headers: HEADERS })).json();
const latest = reports.reports.reduce((a, b) => (b.draw_number > a.draw_number ? b : a));
console.log(latest.draw_number, latest.results.length, "results");
const bundle = await (await fetch(`${BASE}/v1/members/${memberRef}/reports?format=fhir`, { headers: HEADERS })).json();
console.log(bundle.resourceType, bundle.entry.length, "entries");{
"member_id": "PA-0001",
"reports": [
{
"report_id": "3b6f0c9e-52c4-4a43-8f27-7d1f0a6e9c10",
"draw_number": 4,
"drawn_on": "2026-10-01",
"released_at": "2026-11-04T16:31:08.512345Z",
"released_by": "Dr. A. Okafor, Laboratory Director",
"performing_laboratory": { "name": "Example Laboratory", "clia_number": "00D0000000" },
"results": [
{ "protein": "plasma_ptau217", "name": "p-tau217", "unit": "NPQ", "value": 4.1 }
]
}
]
}5. Read what changed
Changes compare each protein's latest released value with the member's own baseline, which is the mean of their first three earlier released draws. Until there is a baseline the status is baseline_building. The response reports findings and which model version read them; it is not a diagnosis.
curl http://localhost:8000/v1/members/PA-0001/changes \
-H "Authorization: Bearer ptk_sandbox_meridian_demo"changes = requests.get(f"{BASE}/v1/members/{member_ref}/changes", headers=HEADERS).json()
for group in changes["groups"]:
print(group["name"], group["status"])const changes = await (await fetch(`${BASE}/v1/members/${memberRef}/changes`, { headers: HEADERS })).json();
for (const group of changes.groups) console.log(group.name, group.status);{
"member_id": "PA-0001",
"model": { "name": "example-model", "version": "1.0.0" },
"groups": [
{
"panel_id": "brain",
"group": "tau",
"name": "Tau",
"status": "rose",
"proteins": [
{ "protein": "plasma_ptau217", "name": "p-tau217", "unit": "NPQ",
"status": "rose", "baseline": 3.2, "latest": 4.1 }
]
}
]
}Next
- Webhooks: headers, signature, retries and secret rotation.
- Embedding the member view and Arc Assist.
- Errors lists what each route answers when something is wrong.