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.
| Scope | Grants |
|---|---|
runs:read | List runs and read one by id — run number, status, timestamps and the protocol. Required by both endpoints. |
outcomes:read | Adds 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.
/api/v1/public/runs runs:read · outcome fields need outcomes:readThe 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"
}
}
]
}/api/v1/public/runs/:id runs:read · outcome fields need outcomes:readOne 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-Signature | sha256= followed by the hex HMAC. |
X-OM-Timestamp | Unix milliseconds at signing time. Part of the signed string. |
X-OM-Event-Id | UUID, stable across retries — use it as your idempotency key. |
X-OM-Event-Type | The 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
2xxcounts 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
3xxis 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 }.
| Status | Means |
|---|---|
400 | A malformed parameter — for example a non-numeric or non-positive limit. |
401 | Missing, malformed, unknown or revoked API key. |
403 | The key is valid but lacks the scope this endpoint requires. |
404 | No such record in your organisation. |
429 | Rate 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