Skip to content
    Developer Documentation

    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-v1

    Authentication

    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.

    curl
    # 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.

    EndpointBeschreibung
    GET /v1/whoamiToken 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}/usageUsage sessions (paginated, from/to)
    GET /v1/equipment/{id}/maintenanceMaintenance logs
    GET /v1/equipment/{id}/calibrationsCalibration records
    GET /v1/equipment/{id}/qualificationsIQ/OQ/PQ qualifications
    GET POST PATCH DELETE /v1/equipment/{id}/remoteRemote-access endpoint of the device (shows the remote button in LabNote)
    GET /v1/reagents · /v1/reagent-stock · /v1/stock-itemsReagent catalog & stock
    GET /v1/stock-movements · /v1/inventoriesMovements & inventories
    GET /v1/devices · /v1/resultsInstruments & instrument results
    GET /v1/auditAudit trail (cursor pagination)
    GET POST /v1/jobs · /{id}Async background jobs (enqueue + poll)
    GET POST /v1/notificationsPush/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.

    EventTriggerPayload fields
    experiment.createdINSERT on experiment_entriesid, display_id, title, status, project_id, author_id, created_at
    experiment.status_changedUPDATE of statusid, display_id, old_status, new_status, updated_at
    experiment.signedfirst time signed_at becomes non-nullid, display_id, signed_by, signed_at
    sample.createdINSERT on samplesid, name, type, status, storage_location_id, created_at
    sample.status_changedUPDATE of statusid, name, old_status, new_status, updated_at
    stock_movement.createdINSERT on reagent_stock_movementsid, stock_item_id, movement_type, quantity_delta, quantity_after, unit, reason, …
    equipment.usage.startedINSERT on equipment_usage_sessionssession_id, equipment_id, user_id, started_at, purpose, project_id, entry_id, settings
    equipment.usage.completedended_at set (manual or auto-close)session_id, equipment_id, started_at, ended_at, duration_minutes, auto_closed, …
    equipment.maintenance.completedmaintenance log with completed_datelog_id, equipment_id, kind, completed_date, completed_by, next_due_date

    Supported wildcards

    experiment.*
    sample.*
    stock_movement.*
    equipment.*
    *

    Envelope

    Every delivery shares the same outer envelope. The actual payload lives in data.

    json
    {
      "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.

    TypeScript
    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);
    }
    Python
    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

    1. Read X-Labnote-Timestamp and reject deliveries older than 5 minutes.
    2. Compare HMAC_SHA256(secret, `${timestamp}.${raw_body}`) in constant time.
    3. Respond 2xx within 15 s — otherwise retries with backoff, then DLQ.
    4. 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.

    curl
    # 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.

    curl
    # 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.

    curl
    # 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.

    bash
    # 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.

    bash
    # 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:

    whoami
    list_experiments
    get_experiment
    list_samples
    list_equipment
    get_equipment
    list_reagent_stock
    list_jobs
    get_job
    enqueue_job
    JSON-RPC
    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