LabNote API & Webhooks
REST endpoints for QMS & ELN integrations, OAuth 2.0, async jobs, signed outbound webhooks, and an MCP server for AI agents — all on one shareable page.
Base URL
https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/api-v1Authentication
Create per-org API keys under Settings → API. Each key carries scopes (read, write, audit:read, admin) and is bound to a single organization. Personal access tokens (PATs) additionally require ?org_id=<uuid> or the X-Org-Id header. Send the token as a Bearer credential.
# Base URL
export LABNOTE_API="https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/api-v1"
curl "$LABNOTE_API/v1/whoami" \
-H "Authorization: Bearer $LABNOTE_API_KEY"
# Personal Access Token (PAT) — org must be selected explicitly:
curl "$LABNOTE_API/v1/experiments?limit=20" \
-H "Authorization: Bearer $LABNOTE_PAT" \
-H "X-Org-Id: <org-uuid>"Resource overview
Stable v1 endpoints. Base URL above; full contract in the OpenAPI spec.
| Endpoint | Beschreibung |
|---|---|
| GET /v1/whoami | Token introspection (org_id, scopes, identity) |
| GET /v1/experiments · /{id} | Experiments + entries (read) |
| GET /v1/projects · /{id} | Projects |
| GET POST /v1/samples · /{id} | Samples (list, create, get) |
| GET /v1/equipment · /{id} | Equipment master data |
| GET /v1/equipment/{id}/usage | Usage sessions (paginated, from/to) |
| GET /v1/equipment/{id}/maintenance | Maintenance logs |
| GET /v1/equipment/{id}/calibrations | Calibration records |
| GET /v1/equipment/{id}/qualifications | IQ/OQ/PQ qualifications |
| GET POST PATCH DELETE /v1/equipment/{id}/remote | Remote-access endpoint of the device (shows the remote button in LabNote) |
| GET /v1/reagents · /v1/reagent-stock · /v1/stock-items | Reagent catalog & stock |
| GET /v1/stock-movements · /v1/inventories | Movements & inventories |
| GET /v1/devices · /v1/results | Instruments & instrument results |
| GET /v1/audit | Audit trail (cursor pagination) |
| GET POST /v1/jobs · /{id} | Async background jobs (enqueue + poll) |
| GET POST /v1/notifications | Push/list in-app notifications |
Outbound event catalog
LabNote emits HMAC-signed webhook deliveries whenever a configured endpoint is subscribed to a matching event. Subscriptions accept exact names and wildcards.
| Event | Trigger | Payload fields |
|---|---|---|
| experiment.created | INSERT on experiment_entries | id, display_id, title, status, project_id, author_id, created_at |
| experiment.status_changed | UPDATE of status | id, display_id, old_status, new_status, updated_at |
| experiment.signed | first time signed_at becomes non-null | id, display_id, signed_by, signed_at |
| sample.created | INSERT on samples | id, name, type, status, storage_location_id, created_at |
| sample.status_changed | UPDATE of status | id, name, old_status, new_status, updated_at |
| stock_movement.created | INSERT on reagent_stock_movements | id, stock_item_id, movement_type, quantity_delta, quantity_after, unit, reason, … |
| equipment.usage.started | INSERT on equipment_usage_sessions | session_id, equipment_id, user_id, started_at, purpose, project_id, entry_id, settings |
| equipment.usage.completed | ended_at set (manual or auto-close) | session_id, equipment_id, started_at, ended_at, duration_minutes, auto_closed, … |
| equipment.maintenance.completed | maintenance log with completed_date | log_id, equipment_id, kind, completed_date, completed_by, next_due_date |
Supported wildcards
Envelope
Every delivery shares the same outer envelope. The actual payload lives in data.
{
"id": "9c0e…-delivery-uuid",
"event": "experiment.signed",
"org_id": "f4a1…",
"delivered_at": "2026-06-19T19:00:00Z",
"data": {
"id": "…",
"display_id": "EXP-0421",
"signed_by": "…",
"signed_at": "2026-06-19T18:59:58Z"
}
}Verifying the HMAC signature
Header X-Labnote-Signature contains t=<ts>,v1=<hex>. Signed bytes = `${timestamp}.${raw_body}`. During secret rotation two v1= values may be present — accept either.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyLabnoteSignature(
rawBody: string,
header: string | null,
secret: string,
toleranceSeconds = 300,
): boolean {
if (!header) return false;
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=", 2)),
);
const ts = Number(parts.t);
if (!ts || Math.abs(Date.now() / 1000 - ts) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
const got = Buffer.from(parts.v1 ?? "", "hex");
const exp = Buffer.from(expected, "hex");
return got.length === exp.length && timingSafeEqual(got, exp);
}import hmac, hashlib, time
def verify_labnote_signature(raw_body: bytes, header: str, secret: str,
tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
ts = int(parts.get("t", "0"))
if abs(time.time() - ts) > tolerance:
return False
expected = hmac.new(
secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Receiver checklist
- Read X-Labnote-Timestamp and reject deliveries older than 5 minutes.
- Compare HMAC_SHA256(secret, `${timestamp}.${raw_body}`) in constant time.
- Respond 2xx within 15 s — otherwise retries with backoff, then DLQ.
- Handle idempotently on id (delivery UUID).
Inbound webhooks
Receive external events. Configure slug, event whitelist and HMAC secret under Settings → Integrations → Inbound. Endpoints can optionally auto-enqueue a background job per delivery.
# Inbound delivery — org_id is part of the path
curl -X POST "https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/inbound-webhook/<org-uuid>/<slug>" \
-H "Content-Type: application/json" \
-H "X-Labnote-Timestamp: $(date +%s)" \
-H "X-Labnote-Signature: t=$(date +%s),v1=<hex>" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{
"event_type": "instrument.measurement_recorded",
"occurred_at": "2026-06-19T20:15:00Z",
"external_id": "HPLC-A-001#run-1",
"payload": { "instrument_serial": "HPLC-A-001", "results": [] }
}'Asynchronous jobs
Long-running operations (audit/GDPR/org exports, equipment lifecycle / maintenance / experiment / risk-assessment PDFs) are dispatched through the jobs queue and polled.
# Enqueue a background job (writes require Idempotency-Key)
curl -X POST "$LABNOTE_API/v1/jobs" \
-H "Authorization: Bearer $LABNOTE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"job_type":"report_export","payload":{"report_id":"…"}}'
# Poll
curl "$LABNOTE_API/v1/jobs/<job-id>" \
-H "Authorization: Bearer $LABNOTE_API_KEY"Notifications
External systems (monitoring, IoT sensors, ticketing gateways) push in-app notifications to individual users, user lists, or roles. Requires write scope; GET returns the calling PAT user's own notifications.
# Push a notification to all admins of the org
# Target: user_id (single) | user_ids (array) | role (string)
curl -X POST "$LABNOTE_API/v1/notifications" \
-H "Authorization: Bearer $LABNOTE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"title": "Temperaturalarm Kühlschrank K-04",
"body": "12.3 °C um 09:12 (Soll ≤ 8 °C)",
"notification_type": "temperature_alarm",
"link_url": "https://monitor.example.com/alerts/4711",
"role": "admin"
}'
# List your own notifications (PAT only)
curl "$LABNOTE_API/v1/notifications?limit=20" \
-H "Authorization: Bearer $LABNOTE_PAT" \
-H "X-Org-Id: <org-uuid>"Rate limits, idempotency & pagination
Guard-rails and conventions that apply to every v1 endpoint.
Rate limits
120 req/min · 5000 req/h per token. 429 → Retry-After.
Idempotency
All writes accept Idempotency-Key. Replays return Idempotent-Replay: true.
Pagination
offset+limit · cursor (cursor bzw. since_created_at+since_id) auf /v1/audit & /v1/jobs.
# Rate limits per token: 120 req/min, 5000 req/h
# On 429 the response carries Retry-After (seconds).
# Idempotency on every write:
# Idempotency-Key: <uuid>
# Replays return: Idempotent-Replay: true
# Pagination:
# - List endpoints: ?limit=100&offset=0 (max limit 500)
# - /v1/audit, /v1/jobs use cursor pagination:
# ?cursor=<opaque> OR ?since_created_at=…&since_id=…OAuth 2.0
Third-party apps integrate via authorization-code flow with refresh-token rotation. Endpoints: /oauth-authorize and /oauth-token.
# 1. Redirect the user to the authorize endpoint:
https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/oauth-authorize
?response_type=code
&client_id=<your-client-id>
&redirect_uri=<your-callback>
&scope=read%20write
&state=<csrf>
# 2. Exchange the code for tokens:
curl -X POST "https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/oauth-token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<code>" \
-d "client_id=<your-client-id>" \
-d "client_secret=<your-client-secret>" \
-d "redirect_uri=<your-callback>"
# 3. Refresh (refresh tokens rotate on every use):
curl -X POST "https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/oauth-token" \
-d "grant_type=refresh_token" \
-d "refresh_token=<rt>" \
-d "client_id=<your-client-id>" \
-d "client_secret=<your-client-secret>"MCP server for AI agents
Streamable HTTP / JSON-RPC. Authenticate like REST (Bearer API key). Available tools:
POST https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/mcp-server
Authorization: Bearer $LABNOTE_API_KEY
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_experiments",
"arguments": { "limit": 5 }
}
}Share this documentation
Public direct link you can share with partners or customers — no login required.
https://labnote-light.com/api-docs