Quick start
Make your first request
Create a key under Integrations → API keys in your organisation. Keep the secret in a server-side secret manager and send it only in the bearer authorisation header.
Base path
/v1/integrations/organisations/{organisation_id} Example
curl --fail-with-body \
--header "Authorization: Bearer $METRA_API_KEY" \
--header "Accept: application/json" \
"https://api.obsidianmetra.com/v1/integrations/organisations/{organisation_id}/equipment?page_size=50" Responses use JSON, timestamps use ISO 8601 UTC and identifiers are UUIDs. Certificate downloads return the validated file media type instead of JSON.
Authentication
Organisation-bound service credentials
A Metra API key is a service credential permanently bound to one organisation. The organisation_id in the request path must match that key. API keys cannot sign in
to the application or call write operations.
Authorization: Bearer metra_live_<credential-id>_<secret> Least privilege
Scopes
Select only the fixed read capabilities the integration needs.
| Scope | Access |
|---|---|
integration:equipment:read | Equipment identity, category, status and current deployment |
integration:structure:read | Clients, sites and locations |
integration:calibration:read | Requirements, due status, dashboard totals and calibration events |
integration:certificates:read | Certificate metadata and private downloads |
integration:quality:read | Out-of-tolerance case identity and workflow state |
Certificate routes require both integration:calibration:read and integration:certificates:read.
Reference
Endpoints
Append each path below to the organisation base path. Every endpoint uses GET.
Equipment and structure
equipment:read / structure:read- GET
/equipmentList equipment and its current deployment - GET
/equipment/{equipment_id}Read one equipment record - GET
/clientsList clients - GET
/sitesList sites - GET
/locationsList hierarchical locations
Calibration planning
calibration:read- GET
/calibration-requirementsList requirements and authoritative due state - GET
/calibration-dashboard/summaryRead aggregate calibration status counts
Events and certificates
calibration:read / certificates:read- GET
/calibration-eventsList retained calibration events - GET
/calibration-events/{event_id}Read one calibration event - GET
/calibration-events/{event_id}/certificatesList certificate metadata - GET
/calibration-events/{event_id}/certificates/{certificate_id}/downloadDownload an authorised private certificate
Quality cases
quality:read- GET
/out-of-tolerance-casesList case identity and workflow state - GET
/out-of-tolerance-cases/{case_id}Read one quality case
Reliable synchronisation
Pagination, filters and checkpoints
List routes use deterministic, opaque cursor pagination. The default page size is 50 and
the maximum is 100. Follow next_cursor exactly; do not construct or modify it.
- Use
changed_sincewhere documented for incremental synchronisation. - Do not combine
cursorandchanged_since. - Store the final
checkpointonly after committing every page. - Run periodic full reconciliation to recover from consumer errors or expired state.
- Unknown query parameters are rejected instead of silently ignored.
Available filters include equipment status, category and site; requirement due state, equipment and due-date range; event status, result and performed-date range; and quality case status and equipment. Date ranges are limited to 366 days.
Operational behaviour
Errors and request limits
| Status | Meaning |
|---|---|
400 | Invalid identifier, cursor, filter or date range |
401 | Missing, malformed, expired or revoked API key |
403 | API key does not have the required scope |
404 | Resource is unavailable within the key's organisation |
429 | Request rate exceeded; retry after the indicated interval |
500 | Unexpected server failure |
The initial limits are 120 requests per API key per minute and 600 requests per
organisation per minute on each API process. A 429 response includes Retry-After. Back off and retry safely.
Keep the response X-Trace-ID when contacting support, but never send the key, authorisation
header, certificate content or sensitive response data.
Signed webhooks
Configure a webhook receiver
Configure webhook endpoints in the application under Integrations → Webhooks. Select the event types your receiver needs and keep the one-time signing secret in a secrets manager.
Destinations must use public HTTPS on port 443. Redirects, embedded credentials, private or reserved networks and cloud metadata destinations are rejected. Metra verifies the destination with a signed challenge before domain events can be delivered, and revalidates DNS before every attempt.
Verification sends a signed webhook.endpoint_verification.v1 challenge. After
validating it, return 204 No Content with Metra-Webhook-Verification: <event-id>. Domain events are not delivered
until this challenge succeeds.
Immutable public contracts
Event catalogue
A webhook identifies a committed occurrence. Use the event's resource ID with a suitably scoped read API key when the receiver needs the current complete resource.
| Event type | Emitted when |
|---|---|
equipment.created.v1 | Equipment is first committed |
equipment.updated.v1 | Externally visible equipment details change |
equipment.status_changed.v1 | Equipment lifecycle status changes |
equipment.deployment_changed.v1 | Current site or location assignment changes |
calibration_requirement.due_status_changed.v1 | Authoritative status crosses current, due soon, due or overdue |
calibration_event.approved.v1 | A calibration event is approved |
calibration_event.rejected.v1 | A submitted calibration event is rejected |
calibration_event.voided.v1 | A calibration event is formally voided |
certificate.available.v1 | A certificate becomes available |
certificate.replaced.v1 | A retained certificate is superseded |
out_of_tolerance.opened.v1 | An investigation is opened |
out_of_tolerance.status_changed.v1 | Its controlled workflow status changes |
out_of_tolerance.closed.v1 | Quality closure is committed |
Example event
{
"id": "7d2639ae-756f-4e39-991f-87c27ecb77d4",
"type": "calibration_event.approved.v1",
"occurred_at": "2026-09-01T12:00:00Z",
"organisation_id": "bd4c6d3a-5e65-4ee5-8381-6e02ec85270a",
"resource": {
"type": "calibration_event",
"id": "dd82ae65-d861-45bf-ab29-4fe16de4b2e6",
"version": 4
},
"data": {
"result": "pass",
"approved_at": "2026-09-01T12:00:00Z"
}
} Payloads are minimal snapshots and never include signing secrets, email addresses, storage keys, certificate bytes, full audit documents, raw measurements or unrestricted investigation notes.
Authentication and replay protection
Verify the exact request bytes
Every attempt includes:
Metra-Webhook-Id: <event-id>
Metra-Webhook-Timestamp: <unix-seconds>
Metra-Webhook-Signature: v1=<lowercase-hex-hmac-sha256>
Content-Type: application/json Metra computes HMAC-SHA-256 over these exact UTF-8 bytes:
<event-id>.<timestamp>.<raw-request-body> Node.js verification example
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyMetraWebhook(rawBody, headers, secret, now = Date.now()) {
const id = headers["metra-webhook-id"];
const timestamp = headers["metra-webhook-timestamp"];
const supplied = (headers["metra-webhook-signature"] ?? "")
.split(",").map(value => value.trim().replace(/^v1=/, ""))
.filter(value => /^[0-9a-f]{64}$/.test(value));
if (!id || !/^\d{10}$/.test(timestamp ?? "") || supplied.length === 0) return false;
if (Math.abs(now / 1000 - Number(timestamp)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(id).update(".").update(timestamp).update(".").update(rawBody)
.digest();
return supplied.some(value => {
const received = Buffer.from(value, "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
});
} - Capture the unmodified raw body before JSON parsing; never re-serialise it for verification.
- Reject timestamps outside a five-minute tolerance before processing.
- Decode the signature and use constant-time byte comparison.
- Deduplicate by
Metra-Webhook-Idbefore applying side effects. - During rotation, try each comma-separated
v1=signature against each currently trusted secret.
Reliable delivery
Design receivers for at-least-once delivery
Return a 2xx response only after the event has been accepted durably. Metra
retries bounded transport failures, timeouts, 408, 409, 425, 429 and 5xx responses with backoff. Most other 4xx responses are terminal.
- The same public event ID is retained across retries and manual redelivery.
- Events may arrive more than once and global ordering is not guaranteed.
- Respond quickly; fetch current resource details asynchronously through the read API.
Retry-Afteris honoured only within safe configured bounds.- Repeated failure may pause an endpoint; resume it after correcting the receiver.
Delivery history stores safe status, timing and attempt metadata, not receiver response bodies. Use event and delivery IDs when troubleshooting and never disclose a signing secret or payload in a support request.
Versioning
Compatibility policy
The /v1 path and versioned response schemas form the compatibility boundary. Optional
fields may be added within v1, so integrations must ignore unknown response fields. Breaking
field, enum or scope changes require a new version or a documented deprecation period.