Skip to content
    Early accessGeneral release planned for 2027Request access

    Developer documentation · October 2026

    LabNote.Light API

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

    Connection and authentication

    REST 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.

    bash
    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>"

    REST endpoints

    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/health

    Service health without a token; does not check integration permissions.

    GET /v1/ping

    Token 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}/remote

    Remote 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/upload

    Result 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/audit

    Audit 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/notifications

    User-scoped reads; send to organisation members with write.

    GET /v1/environmental-points GET POST /v1/environmental-readings

    Environmental 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-results

    Orders with samples and released results; requires sample_intake module.

    Devices and instrument results

    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.

    Outbound webhooks

    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.created

    Laboratory entry created

    id, display_id, title, status, project_id, author_id, created_at

    experiment.status_changed

    Status changed

    id, display_id, old_status, new_status, updated_at

    experiment.signed

    signed_at first set

    id, display_id, signed_by, signed_at

    sample.created

    Sample created

    id, name, type, status, storage_location_id, created_at

    sample.status_changed

    Sample status changed

    id, name, old_status, new_status, updated_at

    stock_movement.created

    Movement created

    id, stock_item_id, movement_type, quantity_delta, quantity_after, unit, reason, …

    equipment.usage.started

    Usage started

    session_id, equipment_id, user_id, started_at, purpose, project_id, entry_id, settings

    equipment.usage.completed

    Usage ended

    session_id, equipment_id, started_at, ended_at, duration_minutes, auto_closed, …

    equipment.maintenance.completed

    Maintenance completed

    log_id, equipment_id, …

    Event filters

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

    Delivery format

    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.

    JSON
    {
      "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"
      }
    }

    Verify webhook signatures

    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.

    Node.js
    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);
      });
    }
    Python
    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)

    Receiver checks

    1. Check timestamps; reject stale deliveries and excessive future timestamps.
    2. Verify the unchanged body and compare candidate v1 signatures in constant time.
    3. Respond with 2xx within the 15-second delivery timeout.
    4. Deduplicate delivery IDs before domain processing.

    Inbound webhooks

    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
    // 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);

    Background jobs

    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.

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

    Notifications

    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.

    bash
    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"}'

    Limits and retries

    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.

    HTTP
    # 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=50

    Test mode and errors

    X-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.

    JSON
    // 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."}}

    OAuth for third parties

    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.

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

    MCP: separate read access

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

    Discuss an integration

    This reference is public. A specific integration requires confirmed organisation access, permissions, client registration and a test environment.