Skip to content

Developers

Build on your calibration data.

The Axiospec Public API is a REST interface to your instruments and calibration records. Read and create instruments, log calibrations to the tamper-evident ledger, and pull your sites and standards into your own systems. Authenticate with a workspace API key and go.

Getting started

Everything you need before your first call

Read this once, then open thefull interactive referencefor the exact request and response shape of every endpoint.

What the API does

The Axiospec Public API is a REST interface to your calibration program. Read and create instruments, log calibrations to the tamper-evident ledger, and read your workspace sites and the compliance standards you have selected.

It is a curated, stable contract, separate from the internal endpoints the web and mobile apps use, so your integration keeps working as the product evolves. Every response is JSON.

Base URL

All endpoints live under a single base URL. Every path in the reference below is relative to it.

https://axiospec.com/api/public/v1

Authentication

Authenticate every request with a per-workspace API key. A workspace admin creates one in the app under Settings then API Keys. Keys are shown once at creation and are prefixed ctk_. Store the key as a secret and never ship it in client-side code.

Send the key on every request as a bearer token:

Authorization: Bearer ctk_your_api_key

A standard X-API-Key header is also accepted if you prefer it: X-API-Key: ctk_your_api_key. A request with no key returns 401.

Plan requirement

The API is available on Professional plans and above. A key that belongs to a workspace on the Free or Starter plan receives a 403 with the code API_ACCESS_TIER_REQUIRED. Upgrade the workspace to enable it.

Scopes

Each key is issued with a scope. A read key can list and fetch. A write key can also create instruments, update them, and log calibrations (write always implies read).

A read-only key that attempts a write receives a 403 with the code INSUFFICIENT_SCOPE, naming the required scope. Issue read-only keys for reporting integrations so they can never change a record.

Rate limits and their headers

Requests are limited to 120 per minute, counted per API key rather than per IP, so one integration cannot starve another and many keys behind one office network are not throttled together.

Every response under the API carries the current window as headers, so you can pace yourself without guessing. X-RateLimit-Limit is the ceiling (120), X-RateLimit-Remaining is how many requests are left in the current window, and X-RateLimit-Reset is the number of seconds until the window resets and Remaining returns to the full limit.

Exceeding the limit returns a 429 with the code RATE_LIMITED. On the 429 a Retry-After header (in seconds) tells you exactly how long to wait; honor it, then retry. Reading these headers rather than hard-coding a delay keeps you fast when there is headroom and polite when there is not.

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 41
Retry-After: 41   (present only on a 429)

Incremental sync

To keep an external system in step without re-reading everything, pull only what changed since your last run. Each collection accepts an updated_since parameter (an ISO-8601 UTC timestamp) that returns only records modified at or after that instant, plus a sort parameter so you can walk them oldest-first and advance a watermark as you go.

The tenant-wide calibration feed, GET /calibrations, is built for exactly this: it returns every calibration across all your instruments in one paged stream, so you do not have to loop instrument by instrument. Sort ascending by updated_at, page through, and remember the created_at of the last record you saw. The ledger is append-only, so a calibration record never changes after it is written and its created_at is its last-modified time; sort=updated_at maps to that instant.

On the next run, pass that saved value as updated_since. Overlap the boundary by a second or two and de-duplicate on the record id to be safe against clock skew. Store the watermark only after you have durably processed the page.

Instruments support the same updated_since and sort parameters (GET /instruments), filtering on the instrument’s last-modified time. The list rows do not include a timestamp field, so for instruments use the wall-clock time you captured just before the request as your next updated_since; the single-instrument GET /instruments/{id} returns created_at and updated_at if you need them.

# First run: no watermark, oldest-first, page through.
GET /api/public/v1/calibrations?sort=updated_at&limit=100

# Save the created_at of the LAST record you processed, e.g.
#   watermark = "2026-07-09T15:30:00Z"

# Next run: only what is new since the watermark.
GET /api/public/v1/calibrations?updated_since=2026-07-09T15:30:00Z&sort=updated_at&limit=100

Filtering instruments

GET /instruments accepts filters so you can fetch a precise slice instead of paging the whole roster. asset_tag and serial_number match an exact value (handy for reconciling a single instrument against an ERP record). status filters by lifecycle state, for example active or retired. site_id scopes to one site.

Two filters are derived from calibration state. compliance_status filters by the computed token, one of COMPLIANT, WARNING, NON_COMPLIANT, or NOT_CALIBRATED. next_due_before takes a date (YYYY-MM-DD) and returns instruments whose next calibration is due before it, which is the query behind a due-soon or overdue worklist. Filters combine, so you can ask for active, non-compliant instruments at one site in a single call.

# Everything overdue or due before a date, oldest instruments first:
GET /api/public/v1/instruments?compliance_status=NON_COMPLIANT&next_due_before=2026-08-01

# Reconcile one instrument by its asset tag:
GET /api/public/v1/instruments?asset_tag=MM-0042

Idempotency

Logging a calibration is the one write that must never be duplicated: the ledger is append-only, so there is no way to undo a double-post. For that reason POST /instruments/{id}/calibrations requires an Idempotency-Key header (any unique string you generate, for example a UUID). It is required, not optional.

If a request is interrupted and you retry it with the same key, the API returns the record it already wrote instead of writing a second one. A missing key returns a 400 with the code IDEMPOTENCY_KEY_REQUIRED. Generate a fresh key per calibration you intend to record.

Creating an instrument (POST /instruments) also honors an Idempotency-Key, but here it is optional. Send one and a retry with the same key returns the instrument the first call created instead of a duplicate, exactly as the calibration log does; the only difference is that the key is not required. If you would rather not manage keys for creates, you can instead deduplicate on your side using the instrument’s asset_tag or serial_number, which are unique within a workspace, before you POST.

Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff

Certificates

Every approved calibration has a branded PDF certificate. Fetch it with GET /calibrations/{calibration_id}/certificate. The response is the PDF itself (Content-Type application/pdf) as an attachment, byte-identical to the certificate the app produces, so you can archive it or attach it to a work order.

A certificate exists only for an approved, current calibration. If the record is voided, superseded by a later entry, or otherwise not certifiable, the request returns a 404 with the code CERTIFICATE_UNAVAILABLE. A calibration id that is not yours, or does not exist, returns a plain 404 that reveals nothing about it.

curl "https://axiospec.com/api/public/v1/calibrations/CALIBRATION_ID/certificate" \
  -H "Authorization: Bearer ctk_your_api_key" \
  -o certificate.pdf

Retiring an instrument

When an instrument leaves service, retire it with POST /instruments/{id}/retire (a write-scope call). This is a soft decommission: the instrument’s status becomes retired and it drops out of the default active list, but nothing is deleted and its calibration history on the ledger is left fully intact for audit. There is no hard delete in the API.

The call returns the updated instrument. It is idempotent: retiring an instrument that is already retired is a no-op that returns the same retired record, so a retry is always safe. Retiring requires a manager or admin key; a read-only or technician key receives a 403.

curl -X POST "https://axiospec.com/api/public/v1/instruments/INSTRUMENT_ID/retire" \
  -H "Authorization: Bearer ctk_your_api_key"

Timestamps and time zones

Every timestamp the API returns is ISO-8601 in UTC, ending in Z, for example 2026-07-09T15:30:00Z. Send timestamps the same way. There are no offset or local-time responses to normalize.

One field is a plain date, not a timestamp: an instrument’s calibration due date. Due dates are computed in your workspace’s configured time zone, so a due date is the calendar day it is due there, and the next_due_before filter takes a date (YYYY-MM-DD) rather than a timestamp. If your systems run in a different zone, compare on the date, not on a midnight-UTC instant.

The error envelope

Every error, on every endpoint, has the same JSON shape: a machine-readable code, a human-readable message, and, for some errors, a details object with the specifics. Branch on code, never on the message text, which may be reworded. The HTTP status still carries meaning (401 versus 403 versus 404), so use it too.

Logging a calibration also enforces the field requirements of the standards your workspace has selected. If a required field is blank, the request returns a 422 with the code FIELD_REQUIREMENTS_UNMET and a missing_fields array, where each entry names the field and which standard requires it, so you can prompt for exactly what is missing.

{
  "code": "FIELD_REQUIREMENTS_UNMET",
  "message": "This calibration is missing fields your workspace's selected standard(s) require: measurement_uncertainty, decision_rule.",
  "details": {
    "missing_fields": [
      { "field": "measurement_uncertainty", "required_by": ["ISO/IEC 17025"] },
      { "field": "decision_rule", "required_by": ["ISO/IEC 17025"] }
    ]
  }
}

Webhooks

Instead of polling the API on a schedule to discover what changed, subscribe a webhook endpoint once and Axiospec delivers each event to your URL as it happens. You get lower latency and far less wasted traffic than re-reading collections you have already seen, and you never miss a change between two polls.

Subscribe with POST /webhooks, passing a url and an optional list of event types to receive. Omit the events field to receive every event (the full catalog is below). The response returns the endpoint’s signing secret exactly once and never again, so copy it straight into your secret store. Managing webhooks needs an admin or manager write-scope key, because the endpoint receives your workspace’s calibration and instrument data.

curl -X POST "https://axiospec.com/api/public/v1/webhooks" \
  -H "Authorization: Bearer ctk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/axiospec",
    "events": ["calibration.approved", "calibration.overdue"],
    "description": "Sync approvals into our QMS"
  }'

Each event is delivered as an HTTP POST whose JSON body is a fixed envelope: { "id", "type", "created_at", "data" }. The id is the stable event id, sent again as the Axiospec-Event-Id header alongside Axiospec-Event-Type, Axiospec-Webhook-Id, and Axiospec-Delivery-Attempt. Delivery is at-least-once, so the same event can arrive more than once (for example after a retry); dedupe on the envelope id.

A delivery counts as failed on any response that is not a 2xx, and that includes a 3xx redirect: a redirect could point at an internal address, so it is never followed. Transport errors and timeouts count as failures too. Failed deliveries are retried on an exponential backoff that spans roughly three days, after which the delivery is marked exhausted. An endpoint whose recent deliveries all exhaust is automatically disabled so a dead or hostile URL stops consuming capacity. Endpoints must be HTTPS, and a URL that resolves to a private or internal address is rejected when you subscribe.

Inspect what was sent with GET /webhooks/{id}/deliveries, which pages an endpoint’s delivery log and accepts a status filter (pending, failed, succeeded, exhausted). To replay one delivery, POST /webhooks/{id}/deliveries/{delivery_id}/retry: it resets that delivery to pending and due now so the next dispatch resends it, with a fresh retry horizon. Rotate an endpoint’s secret with POST /webhooks/{id}/rotate-secret (the old secret stops verifying immediately), and stop deliveries with DELETE /webhooks/{id}.

Webhook event catalog

These are the event types a webhook can subscribe to. List the ones you want when you subscribe, or omit the events field to receive all of them. The same catalog is available at GET /webhooks/events for programmatic discovery.

calibration.created
calibration.approved
calibration.rejected
calibration.corrected
calibration.voided
calibration.due_soon
calibration.overdue
instrument.created
instrument.updated
instrument.retired
instrument.status_changed

Verifying webhook signatures

Every delivery carries an Axiospec-Signature header of the form t=<unix-seconds>,v1=<hex>. Verify it before you trust a payload: a valid signature proves the request came from Axiospec and that the body was not altered in transit.

Read the Axiospec-Signature header and split it on the comma into its t= part (a Unix timestamp in seconds) and its v1= part (a lowercase hex HMAC). Recompute HMAC-SHA256, keyed by your endpoint’s signing secret, over the string formed by the timestamp, a literal dot, and the exact raw request body, that is f"{t}.{raw_body}". Compare your hex digest against the v1 value with a constant-time comparison, never an ordinary equality check.

Sign the raw bytes exactly as received, before any JSON parsing or re-serialization, so your input matches what was signed. Reject the delivery if the digests do not match, or if t is more than about five minutes old, which bounds how long a captured request could be replayed against you.

# Axiospec-Signature: t=1720625400,v1=3f6a9c...e1
t, v1    = split the header on "," then read the "t=" and "v1=" values
signed   = t + "." + raw_request_body        # the exact bytes received
expected = hex(hmac_sha256(secret, signed))  # lowercase hex digest

if not constant_time_equals(expected, v1):
    reject        # signature mismatch, do not trust the payload
if now_unix_seconds() - int(t) > 300:
    reject        # older than ~5 minutes, treat as a possible replay

accept            # then dedupe on the envelope id (Axiospec-Event-Id)

Try it: two examples

Replace ctk_your_api_keywith your key and INSTRUMENT_IDwith an instrument id from the list call.

1. List instruments

curl "https://axiospec.com/api/public/v1/instruments?status=active&limit=25" \
  -H "Authorization: Bearer ctk_your_api_key"

2. Log a calibration

Note the required Idempotency-Key header. Retrying with the same key returns the record that was already written instead of logging a duplicate.

curl -X POST "https://axiospec.com/api/public/v1/instruments/INSTRUMENT_ID/calibrations" \
  -H "Authorization: Bearer ctk_your_api_key" \
  -H "Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff" \
  -H "Content-Type: application/json" \
  -d '{
    "result": "PASS",
    "performed_at": "2026-07-09T15:30:00Z",
    "nominal_value": "10.00 V",
    "tolerance": "±0.1%",
    "as_found_reading": "10.01 V",
    "as_left_reading": "10.00 V",
    "certificate_number": "CERT-2026-0142"
  }'

Error codes

Every code the API returns, its HTTP status, and what it means. Branch on the code.

CodeHTTPMeaning
UNAUTHORIZED401No API key, or the key is invalid, revoked, or expired.
API_ACCESS_TIER_REQUIRED403The workspace is on the Free or Starter plan. The API needs Professional or above.
INSUFFICIENT_SCOPE403A read-only key attempted a write. Issue a write-scope key.
FORBIDDEN403The key is valid but the action is not permitted for its role, for example a non-manager key retiring an instrument.
INSUFFICIENT_ROLE403The action needs an admin or manager role and the key’s role is lower, for example a technician key managing a webhook.
NOT_FOUND404The resource does not exist, or is outside this key’s tenant or site scope. Returned identically in both cases so nothing leaks.
CERTIFICATE_UNAVAILABLE404The calibration exists but is not certifiable (not approved, voided, or superseded).
IDEMPOTENCY_KEY_REQUIRED400A calibration log was sent without the required Idempotency-Key header.
INVALID_REQUEST400 or 422A request that was malformed or failed a coded validation, for example a bad query token, an unusable document key, or an invalid webhook field. Document and webhook validation returns 422; a generic bad request returns 400. Branch on the code, the status is secondary.
VALIDATION_ERROR422One or more fields failed validation. details.errors lists each field and why.
FIELD_REQUIREMENTS_UNMET422A calibration was missing a field your selected standard(s) require. details.missing_fields lists them.
INVALID_WEBHOOK_URL422The webhook url is not usable: it must be HTTPS, and a URL that resolves to a private or internal address is rejected.
CONFLICT409The request conflicts with the current state of the resource.
WEBHOOK_LIMIT_REACHED409The workspace already has the maximum number of webhook endpoints. Delete one before adding another.
DELIVERY_CONFLICT409A webhook delivery cannot be retried in its current state, for example replaying a delivery that is not yet resolved.
OBJECT_NOT_UPLOADED409A document was registered for a key whose file was never uploaded. Upload the file to the presigned URL first, then register it.
METHOD_NOT_ALLOWED405That HTTP method is not supported on this path.
RATE_LIMITED429The 120-per-minute limit was exceeded. Wait for the Retry-After seconds, then retry.
INTERNAL_ERROR500An unexpected server error. Safe to retry a read; retry a calibration log with the same Idempotency-Key.

Versioning and stability

This is v1, reflected in the /api/public/v1 base path. It is a curated, stable contract kept deliberately separate from the internal endpoints the apps use.

Additive changes are not breaking, and we make them without bumping the version: new endpoints, new optional request fields, new fields on a response, and new values in an enumerated field (for example a new compliance_status token). Write your client to tolerate them. Ignore response fields you do not recognize rather than failing, and treat an unfamiliar enum value as a pass-through string rather than a hard error.

Breaking changes, which we avoid, would include removing or renaming a field, changing a field’s type, or changing an endpoint’s meaning. If we ever had to make one, it would ship under a new version path (/api/public/v2), the old version would keep working through a clearly communicated deprecation window, and we would announce it in the changelog below before removing anything.

Key rotation and storage

A key is shown in full exactly once, at the moment you create it. We store only a salted hash (SHA-256), never the key itself, so it cannot be recovered or emailed to you later. Copy it into your secret store then. The app can show you a non-secret prefix (ctk_AbC1…) afterward to help you tell keys apart, but never the whole key again.

To rotate a key, create a new one, deploy it, then revoke the old one. Revoking is immediate and permanent; the key is deactivated (never hard-deleted, so your audit history stays intact) and every later request with it returns 401 UNAUTHORIZED. Issue separate keys per integration and read-only keys for anything that only reports, so you can rotate or revoke one without disturbing the others.

Changelog

v1.12026-07-10

  • Webhooks: subscribe to events (calibration logged/approved, instrument due-soon/overdue, and more) with HMAC-signed, retried delivery.
  • Field requirements: GET /standards/field-requirements publishes the calibration fields your selected standards require, so you can build a valid calibration payload before you POST.
  • Due worklist: GET /due returns instruments due or overdue within a horizon, each with its authoritative compliance status, for scheduling and dashboards.
  • Deletion tombstones: pass include=retired (instruments) or include=voided (calibrations) on the incremental feeds so a record that was retired or voided appears in the delta instead of silently disappearing. Off by default; existing syncs are unchanged.
  • Attachments: request a presigned upload URL, attach a document to an instrument (optionally to a specific calibration), list a record’s documents, and fetch a short-lived download URL.

v12026-07-09

  • Incremental sync: updated_since and sort on instruments and calibrations, plus a tenant-wide GET /calibrations feed.
  • Instrument filtering: asset_tag, serial_number, compliance_status, and next_due_before.
  • Certificate retrieval: GET /calibrations/{id}/certificate returns the calibration’s PDF.
  • Instrument retire: POST /instruments/{id}/retire soft-decommissions an instrument, leaving the ledger intact.
  • Rate-limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After on a 429) on every response.
  • A uniform error envelope ({ code, message, details? }) across every endpoint.

v1Initial release

  • Read and create instruments, log calibrations to the tamper-evident ledger, and read sites and selected standards.
  • Bearer or X-API-Key authentication, read and write scopes, Professional-and-above access, 120-per-minute rate limiting, and the paged list envelope.

Reference

The full REST reference

Every endpoint, parameter, request body, and response, generated from the API's OpenAPI specification and rendered as a searchable, full-screen reference.

The reference opens in a new tab with a searchable navigator, request and response schemas, and copy-paste examples for every operation. Prefer to generate a client? The OpenAPI spec above drives code generators in every major language.

---