Skip to content

Enterprise API

Build OrganMatch into your stack

Scoped API keys, a read API for experiment runs, and HMAC-signed webhooks with automatic retries — so run data reaches your ELN, warehouse or Slack without anyone copying it by hand.

Creating API keys and webhook endpoints requires an Enterprise plan and an organisation admin. Reading these docs does not.

Authentication

Every request carries an organisation API key as a bearer token. Keys are created by an organisation owner or admin in the Enterprise console. The secret is shown once at creation; we store a SHA-256 hash of it plus the first few characters as a display prefix, so the secret itself cannot be recovered — if it is lost, revoke the key and create another.

curl https://api.organthis.com/api/v1/public/runs \
  -H "Authorization: Bearer om_live_…"

Scopes

Keys are least-privilege and read-only. A request for an endpoint outside the key's scopes returns 403; a missing, unknown or revoked key returns 401.

ScopeGrants
runs:readList runs and read one by id — run number, status, timestamps and the protocol. Required by both endpoints.
outcomes:readAdds the result fields to those same responses: outcome status, viability, yield, measurements and notes. Without it those fields are omitted entirely.

Endpoints

All responses are JSON and wrap the result in a data key. Runs are scoped to the organisation the key belongs to — a key can never read another tenant's data.

GET /api/v1/public/runs runs:read · outcome fields need outcomes:read

The organisation's runs, newest first. limit defaults to 50 and is capped at 200; it must be a positive integer, and anything else returns 400. The outcomeStatus and viabilityPct fields below appear only for a key that also carries outcomes:read.

{
  "data": [
    {
      "id": "clx8f2n0a0001",
      "runNumber": 42,
      "status": "COMPLETED",
      "outcomeStatus": "SUCCESS",
      "viabilityPct": 78,
      "createdAt": "2026-08-20T08:00:00.000Z",
      "completedAt": "2026-08-27T09:14:00.000Z",
      "protocol": {
        "slug": "intestinal-organoid-v3",
        "title": "Intestinal organoid expansion",
        "organType": "INTESTINE"
      }
    }
  ]
}
GET /api/v1/public/runs/:id runs:read · outcome fields need outcomes:read

One run, with the outcome detail the list omits — yieldValue, yieldUnit, outcomeMeasurements and outcomeNotes. Every one of those, and the two outcome fields from the list, is withheld unless the key carries outcomes:read — a key without it still sees that the run exists and whether it finished. Returns 404 if the run does not exist or belongs to another organisation.

Webhooks

Register an HTTPS endpoint in the console and subscribe it to the events you care about. Each endpoint gets its own signing secret. Every event is delivered as a POST with a versioned envelope; the event-specific fields live under data.

{
  "id": "5e9c1b0e-4b2a-4c3d-9f1a-6d8e2b7c0a11",
  "type": "run.completed",
  "version": "v1",
  "createdAt": "2026-08-27T09:14:02.113Z",
  "data": { }
}

Headers

X-OM-Signaturesha256= followed by the hex HMAC.
X-OM-TimestampUnix milliseconds at signing time. Part of the signed string.
X-OM-Event-IdUUID, stable across retries — use it as your idempotency key.
X-OM-Event-TypeThe event type, so you can route before parsing.

Verifying a signature

The signature is an HMAC-SHA256 of {timestamp}.{raw body} keyed with the endpoint's signing secret. Compare it in constant time, and reject the request if it does not match — a valid signature is the only proof the request came from us.

import { createHmac, timingSafeEqual } from 'crypto';

// body must be the RAW request body, before any JSON parsing.
export function verify(body, headers, signingSecret) {
  const timestamp = headers['x-om-timestamp'];
  const received  = String(headers['x-om-signature'] || '').replace(/^sha256=/, '');

  // Reject replays. The delivery attempt itself times out after 10s.
  const ageMs = Date.now() - Number(timestamp);
  if (!timestamp || Number.isNaN(ageMs) || Math.abs(ageMs) > 5 * 60 * 1000) return false;

  const expected = createHmac('sha256', signingSecret)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(received, 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Delivery and retries

  • Any 2xx counts as accepted.
  • Each attempt times out after 10 seconds — acknowledge fast and do your work afterwards.
  • Up to 6 attempts with exponential backoff, then the delivery is dead-lettered and stays visible in the console.
  • Retries reuse the same X-OM-Event-Id, so deduplicate on it.
  • Two failures are terminal and are not retried at all: the endpoint being deactivated, and its URL failing the address check on a later attempt. Both dead-letter immediately on the first attempt that sees them.
  • Redirects are not followed. A 3xx is recorded as a failed attempt — register the final URL directly.

Endpoint URL rules

Endpoints are checked when you register them and again before every delivery attempt, with the hostname re-resolved each time. A URL is refused if it:

  • is not https, or uses a port other than 443
  • contains a username or password
  • resolves to a private, loopback, link-local or otherwise non-public address

These checks reduce, but cannot completely eliminate, the window between a DNS lookup and the connection that follows it. We do not treat your endpoint as a trusted network location.

Test before you wire anything up

Every endpoint in the console has a Send test event button. It sends a real signed ping.test through the same dispatcher, with the same signing and the same retries, and the result appears in the delivery log — so you can confirm your receiver and your signature check work before a single run exists. The log's copy control gives you the exact bytes the signature was computed over, so you can reproduce the HMAC yourself. Limited to one test per endpoint every 30 seconds.

Event catalogue

Envelope version v1. Fields may be added to data without a version bump, so parse permissively and ignore what you do not recognise. New event types are added over time — route on type and ignore what you have not subscribed to.

run.created A run is started from a protocol in the lab book. Only runs attributed to the organisation emit events — a member’s personal run does not.
{
  "experimentId": "clx8f2n0a0001",
  "runNumber": 42,
  "protocol": { "slug": "intestinal-organoid-v3", "title": "Intestinal organoid expansion" }
}
run.completed A run’s status changes to COMPLETED.
{
  "experimentId": "clx8f2n0a0001",
  "runNumber": 42
}
outcome.recorded An outcome is captured or changed on a run.
{
  "experimentId": "clx8f2n0a0001",
  "outcomeStatus": "SUCCESS",
  "viabilityPct": 78
}
deviation.recorded A coded reason is logged for a run diverging from plan. Note there is no corresponding removal event yet — if you mirror deviations, reconcile against the run rather than assuming the feed is append-only.
{
  "experimentId": "clx8f2n0a0001",
  "runNumber": 42,
  "deviationId": "clx8f2n0a0003",
  "code": "REAGENT_LOT",
  "phaseNumber": 2
}
ping.test Sent on demand from the console. Not subscribable — it goes to the one endpoint you choose.
{
  "message": "This is a test event from OrganMatch. No run or outcome was created.",
  "endpointId": "clx8f2n0a0002",
  "triggeredBy": "you@lab.org"
}

Errors and limits

Errors use standard status codes with a JSON body carrying a message — except 429, which returns { type, title, status, detail }.

StatusMeans
400A malformed parameter — for example a non-numeric or non-positive limit.
401Missing, malformed, unknown or revoked API key.
403The key is valid but lacks the scope this endpoint requires.
404No such record in your organisation.
429Rate limited. Honour the Retry-After header.

Rate limit — read this before scheduling a sync

API-key traffic is currently limited to 100 requests per minute, counted per source IP address rather than per key — so it is shared with other traffic from the same egress address. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. Page with limit rather than issuing many small requests, and if you need a higher or key-scoped ceiling for a nightly sync, ask us — it is a configuration change, not a rebuild.

Ready to connect something?

Create a scoped key and register an endpoint in the Enterprise console. If you need an event or a field that is not here yet, tell us what you are building — the catalogue grows with what design partners actually integrate.

Open the Enterprise console