GET /v1/healthService health without a token; does not check integration permissions.
Cookies and similar technologies
We use strictly necessary cookies to ensure platform functionality. With your consent, we may also set analytics cookies. You can change your choice at any time. More in the privacy policy.
Developer documentation · October 2026
REST interfaces, device integration and webhooks: endpoints, permissions and examples for technical integration. MCP and OAuth are documented separately.
REST base URL
https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/api-v1REST accepts organisation API keys and personal access tokens (PATs) as Bearer credentials, not arbitrary sign-in tokens. API keys are bound to one organisation. Unbound PATs require X-Org-Id or ?org_id=<uuid>; an explicit organisation must match a bound token. PAT membership is checked on each request. Store tokens server-side, never in browser code or public repositories.
read permits ordinary reads; write includes read. The audit endpoint requires audit:read. Scopes do not replace role, department or record permissions. Deleting generic records requires an administrator role. Equipment, instrument-result and laboratory-order endpoints require the corresponding module to be enabled. Signatures cannot be set through generic REST writes.
export LABNOTE_API="https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/api-v1"
# LABNOTE_API_KEY: set securely in your environment
curl -sS "$LABNOTE_API/v1/ping" \
-H "Authorization: Bearer $LABNOTE_API_KEY"
curl -sS "$LABNOTE_API/v1/experiments?limit=20" \
-H "Authorization: Bearer $LABNOTE_PAT" \
-H "X-Org-Id: <org-uuid>"Append these paths to the REST base URL. Methods apply to the listed collection or item path; writable fields and additional permissions differ by resource. This reference describes the implemented code, not blanket availability for every organisation.
GET /v1/healthService health without a token; does not check integration permissions.
GET /v1/pingToken identity, organisation and resource registry.
GET POST /v1/experiments
GET PATCH PUT DELETE /v1/experiments/{id}Generic CRUD; allowlisted fields and record permissions.
GET POST /v1/projects
GET PATCH PUT DELETE /v1/projects/{id}Generic CRUD; allowlisted fields and record permissions.
GET POST /v1/samples
GET PATCH PUT DELETE /v1/samples/{id}Generic CRUD; allowlisted fields and record permissions.
GET POST /v1/equipment
GET PATCH PUT DELETE /v1/equipment/{id}Generic CRUD; allowlisted fields and record permissions. Requires lab_equipment.
GET POST /v1/reagents
GET PATCH PUT DELETE /v1/reagents/{id}Generic CRUD; allowlisted fields and record permissions.
GET POST /v1/stock-items
GET PATCH PUT DELETE /v1/stock-items/{id}Generic CRUD; allowlisted fields and record permissions.
GET POST /v1/inventories
GET PATCH PUT DELETE /v1/inventories/{id}Generic CRUD; allowlisted fields and record permissions.
GET POST /v1/stock-movements
GET DELETE /v1/stock-movements/{id}Read and create movements; no updatable fields.
GET POST /v1/equipment/{id}/{sub}
PATCH PUT /v1/equipment/{id}/{sub}/{subId}sub: usage, maintenance, calibrations or qualifications. Records of an accessible device.
GET POST PUT PATCH DELETE
/v1/equipment/{id}/remoteRemote link; POST/PUT upsert, PATCH existing link, DELETE 204. Not a remote-control protocol.
GET POST /v1/devices
DELETE /v1/devices/{id}Register/list instrument devices; deletion requires an administrator. No individual GET route.
GET POST /v1/results
GET /v1/results/{id}
GET /v1/results/{id}/chart
POST /v1/results/uploadResult ingestion, raw-file upload and result data; requires analytics_qc module.
GET POST /v1/connectors
GET DELETE /v1/connectors/{id}Connector heartbeat and device sync; deletion requires an administrator.
GET /v1/auditAudit trail with audit:read and a two-part cursor.
GET POST /v1/jobs
GET /v1/jobs/{id}Submit allowlisted background jobs and read status.
GET POST /v1/notificationsUser-scoped reads; send to organisation members with write.
GET /v1/environmental-points
GET POST /v1/environmental-readingsEnvironmental monitoring: list points and ingest readings (single or batch); requires analytics_qc module.
GET POST /v1/lab-orders
GET /v1/lab-orders/{id}
GET /v1/lab-resultsOrders with samples and released results; requires sample_intake module.
REST accepts connector status, device records and instrument results. A local OPC UA/LADS agent is a separate integration; this API does not supply a universal device driver. Send results to /v1/results and raw files to /v1/results/upload (multipart/form-data, file und device_id). Size limits: 25 MiB, or 50 MiB for mzML. API keys can be bound to a connector and devices; PATs cannot. Automatic device registration during result ingestion requires devices:register or administrator scope. Validate device-specific access and result assignment before use.
Configured webhook endpoints subscribe to exact event names or wildcards. Deliveries run asynchronously to permitted HTTP(S) URLs; internal network addresses are blocked. Failed deliveries have up to five attempts in total with exponential backoff. No fixed delivery time is committed.
experiment.createdLaboratory entry created
id, display_id, title, status, project_id, author_id, created_at
experiment.status_changedStatus changed
id, display_id, old_status, new_status, updated_at
experiment.signedsigned_at first set
id, display_id, signed_by, signed_at
sample.createdSample created
id, name, type, status, storage_location_id, created_at
sample.status_changedSample status changed
id, name, old_status, new_status, updated_at
stock_movement.createdMovement created
id, stock_item_id, movement_type, quantity_delta, quantity_after, unit, reason, …
equipment.usage.startedUsage started
session_id, equipment_id, user_id, started_at, purpose, project_id, entry_id, settings
equipment.usage.completedUsage ended
session_id, equipment_id, started_at, ended_at, duration_minutes, auto_closed, …
equipment.maintenance.completedMaintenance completed
log_id, equipment_id, …
Event filters
experiment.* · sample.* · stock_movement.* · equipment.* · *
id identifies the delivery, event the event type and data the event payload. Receivers must handle duplicate deliveries by delivery ID. This example shows a signed laboratory entry; IDs and timestamps are illustrative.
{
"id": "11111111-1111-4111-8111-111111111111",
"event": "experiment.signed",
"org_id": "22222222-2222-4222-8222-222222222222",
"delivered_at": "2026-10-07T03:00:00Z",
"data": {
"id": "33333333-3333-4333-8333-333333333333",
"display_id": "EXP-0421",
"signed_by": "44444444-4444-4444-8444-444444444444",
"signed_at": "2026-10-07T02:59:58Z"
}
}X-LabNote-Signature contains t=<Unix seconds>,v1=<hex HMAC>. HMAC-SHA256 signs the unchanged bytes of <timestamp>.<raw body>. Secret rotation can produce two v1 values: one valid match is sufficient. Parse JSON only after verification. These examples handle multiple signatures, malformed headers and a ±300-second time tolerance.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyLabnoteSignature(rawBody, header, secret,
toleranceSeconds = 300) {
if (typeof header !== "string") return false;
const parts = header.split(",").map(p => p.trim());
const timestamps = parts.filter(p => p.startsWith("t="));
const signatures = parts.filter(p => p.startsWith("v1="))
.map(p => p.slice(3));
if (timestamps.length !== 1 || !signatures.length) return false;
const stamp = timestamps[0].slice(2);
if (!/^[0-9]+$/.test(stamp)) return false;
const ts = Number(stamp);
if (!Number.isSafeInteger(ts) ||
Math.abs(Date.now() / 1000 - ts) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret)
.update(stamp + ".").update(rawBody).digest();
return signatures.some(sig => {
if (!/^[a-f0-9]{64}$/i.test(sig)) return false;
return timingSafeEqual(Buffer.from(sig, "hex"), expected);
});
}import hmac, hashlib, time, re
def verify_labnote_signature(raw_body: bytes, header: str,
secret: str, tolerance: int = 300) -> bool:
if not isinstance(header, str):
return False
parts = [p.strip() for p in header.split(",")]
stamps = [p[2:] for p in parts if p.startswith("t=")]
sigs = [p[3:] for p in parts if p.startswith("v1=")]
if len(stamps) != 1 or not re.fullmatch(r"[0-9]{1,16}", stamps[0]):
return False
if abs(time.time() - int(stamps[0])) > tolerance:
return False
expected = hmac.new(secret.encode(), stamps[0].encode() + b"." +
raw_body, hashlib.sha256).hexdigest()
return any(re.fullmatch(r"[a-fA-F0-9]{64}", sig) and
hmac.compare_digest(expected, sig.lower()) for sig in sigs)Separate POST endpoint: /inbound-webhook/{org_id}/{slug}, not under /api-v1/v1. Authentication uses the configured HMAC secret. The event type comes from event or X-LabNote-Event, not event_type. Maximum message size: 1,000,000 bytes. HTTP 202 confirms storage or optional job submission, not successful domain processing. Disallowed events also receive 202 with status: ignored. Invalid signatures and unknown or inactive endpoints uniformly return 404. external_id alone is not a deduplication key; reuse the same Idempotency-Key for retries.
// Node.js: sign the exact body sent to the receiver
import { createHmac, randomUUID } from "node:crypto";
const url = "https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/inbound-webhook/<org-uuid>/<slug>";
const body = JSON.stringify({ event: "instrument.measurement_recorded",
payload: { sample_code: "S-001", value: 12.3, unit: "mg/L" } });
const stamp = String(Math.floor(Date.now() / 1000));
const sig = createHmac("sha256", process.env.LABNOTE_WEBHOOK_SECRET)
.update(stamp + "." + body).digest("hex");
const key = randomUUID(); // retain this key and body for retries
const response = await fetch(url, { method: "POST", headers: {
"Content-Type": "application/json",
"X-LabNote-Signature": "t=" + stamp + ",v1=" + sig,
"Idempotency-Key": key
}, body });
console.log(response.status);POST /v1/jobs expects job_type and optional input and returns 202 with data. GET reads status. audit_export, biosafety_export, gdpr_export and org_data_export require an administrator role. Other allowed types are equipment_lifecycle_pdf, maintenance_report_pdf, equipment_book_pdf, experiment_entry_pdf, risk_assessment_pdf and box_layout_pdf. Creators can see their own jobs; administrators can inspect organisation-wide jobs. Submission guarantees neither successful completion nor a specific file contract; input depends on the job type.
# Read existing jobs; creation needs job-specific input
curl -sS "$LABNOTE_API/v1/jobs?limit=20" \
-H "Authorization: Bearer $LABNOTE_API_KEY"
# POST body format (replace input with the job's required fields):
# {"job_type":"experiment_entry_pdf","input":{"entry_id":"<entry-uuid>"}}
# Poll an accepted job:
curl -sS "$LABNOTE_API/v1/jobs/<job-uuid>" \
-H "Authorization: Bearer $LABNOTE_API_KEY"POST requires write and targets organisation members through user_id, user_ids or role. GET with read returns notifications for the user associated with the token, not an organisation-wide list. Links must begin with http:// or https://. Inspect Warning headers and pagination information when recipients or results are capped.
curl -sS -X POST "$LABNOTE_API/v1/notifications" \
-H "Authorization: Bearer $LABNOTE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <stable-unique-request-key>" \
-d '{"title":"K-04 temperature alert","body":"12.3 °C; limit 8 °C",
"notification_type":"temperature_alarm","role":"admin"}'REST code defaults: 120 requests/minute and 5,000/hour per token; deployments can configure different values. For this token limit, 429 currently returns Retry-After: 60, not the exact remaining hourly window. Additional route-specific limits exist. Idempotency-Key is optional and not handled on every write path: supported by generic CRUD, jobs, notifications and equipment sub-resource POST; not universally by connector POST, remote access, result ingestion or equipment sub-resource PATCH/PUT. Replays preserve the original status and set Idempotent-Replay: true; default retention is 24 hours.
Generic lists: limit (default 50, max. 500), offset, order_by, order=asc|desc and allowlisted equality filters. Equipment sub-resources: default 100, max. 500, from/to. Audit: from/to date filters, default 100, max. 1,000; next_cursor supplies since_created_at and since_id for the next query. Jobs: default 50, max. 200; cursor is the enqueued_at timestamp returned in next_cursor. Laboratory results are capped at 2,000 rows and preceding sample lookups at 1,000; inspect Warning headers. Not every list supports offset or a cursor.
# Generic list
GET /v1/samples?limit=50&offset=0&order_by=created_at&order=desc
# Audit: use BOTH fields returned in next_cursor
GET /v1/audit?since_created_at=<ISO-timestamp>&since_id=<uuid>&limit=100
# Jobs: cursor is an enqueued_at timestamp
GET /v1/jobs?cursor=<ISO-timestamp>&limit=50X-Test-Mode: true is handled only by generic CRUD. It checks authentication and preliminary write permissions and echoes would_apply, but skips actual field validation and record mutation. It is not a general dry-run for jobs, webhooks or results and does not fully validate a write. Use an isolated test organisation for real integration tests. Handle the different error formats; X-Request-Id is not universally implemented for api-v1.
// Most REST route errors
{"error":{"code":"insufficient_scope","message":"Token lacks 'read' scope."}}
// Shared idempotency conflict (different shape)
{"data":null,"error":"idempotency_key_conflict","meta":{"message":"Same Idempotency-Key used with a different payload."}}The separate oauth-authorize and oauth-token functions implement an authorization-code flow for registered clients with a client secret and rotating refresh tokens. This is not MCP authentication. Consent binds access to the selected organisation; admin and audit:read are removed for users without an administrator role. This documentation does not promise a public PKCE flow or availability for arbitrary clients; confirm client registration and deployment before integration.
# Separate REST OAuth functions (registered confidential clients)
https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/oauth-authorize
https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/oauth-token
# Not interchangeable with the MCP Supabase OAuth issuer.The MCP module defines exactly four tools: whoami, list_experiments, list_samples and list_equipment. Lists are user-scoped and permission-filtered; there are no write or job tools. MCP uses Supabase OAuth as the signed-in user, not a REST API key. The function name is mcp, not mcp-server. Deployment and successful authorised calls are not presented as live-verified here.
whoamilist_experimentslist_sampleslist_equipment# Defined MCP function; confirm deployment before connecting
https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/mcp
# Discover with an OAuth-capable MCP client: tools/list
# Read-only: whoami, list_experiments, list_samples, list_equipment
# REST API keys are not MCP credentials.This reference is public. A specific integration requires confirmed organisation access, permissions, client registration and a test environment.