Download OpenAPI specification:
An account in Calira (formerly Clustermarket) represents an organisation, department, or lab. We have a hierarchical structure following these rules:
graph TD
org[Organisation]
org-->lab0[Lab]
dep1[Department]
dep2[Department]
org-->dep1
dep1-->lab1[Lab]
dep1-->lab2[Lab]
org-->dep2
dep2-->lab3[Lab]
il[Independent Lab]
All endpoints in the Calira (formerly Clustermarket) API support pagination to efficiently handle large datasets. The API uses a custom pagination system with header-based metadata.
page: The page number to retrieve (1-based indexing, default: 1)per_page: Number of records per page (1-100, default: 100, maximum: 100)The API provides pagination metadata in response headers:
X-Total-Records: Total number of records availableX-Current-Page: Current page numberX-Per-Page: Number of records per page# Get first page (default behavior)
GET /equipment?account=123
# Headers: X-Total-Records: 250, X-Current-Page: 1, X-Per-Page: 100
# Get first page with 50 records
GET /equipment?account=123&per_page=50
# Headers: X-Total-Records: 250, X-Current-Page: 1, X-Per-Page: 50
# Get second page with 50 records
GET /equipment?account=123&per_page=50&page=2
# Headers: X-Total-Records: 250, X-Current-Page: 2, X-Per-Page: 50
# Get third page with 25 records
GET /equipment?account=123&per_page=25&page=3
# Headers: X-Total-Records: 250, X-Current-Page: 3, X-Per-Page: 25
# Example: Get all equipment by iterating through pages
# Page 1: GET /equipment?account=123&per_page=100&page=1
# Page 2: GET /equipment?account=123&per_page=100&page=2
# Page 3: GET /equipment?account=123&per_page=100&page=3
# Continue until X-Current-Page * X-Per-Page >= X-Total-Records
Complete error reference for all upcoming write endpoints. Each endpoint section below also repeats its relevant errors.
OAuth / authorisation — { "error": "<code>", "error_description": "<message>" }
| HTTP | error |
When |
|---|---|---|
401 |
unauthorised |
Missing, invalid, or expired Bearer token |
403 |
unapproved_application |
OAuth application not approved by Calira |
403 |
resource_authorisation_failure |
Missing OAuth scope, lab not authorised for the application, or user lacks permission |
404 |
resource_not_found |
Unknown API path, or record not found where no endpoint-specific handler exists (e.g. unknown booking ID) |
500 |
internal_server_error |
Unexpected server error — retry with exponential backoff |
Equipment resource not found — { "errors": ["<code>"], "error_messages": "<message>" }
| HTTP | errors |
Endpoint |
|---|---|---|
404 |
account_not_found |
POST /equipment — account_id missing or does not exist |
404 |
equipment_not_found |
PATCH /equipment/{id} — equipment ID in URL does not exist |
404 |
not_found |
GET /equipment/contact_people — account_id omitted, or account/equipment not found |
Validation — { "success": false, "message": "...", "errors": [{ "attribute": "...", "message": "..." }], "error_messages": ["..."] } on 422
POST /bookings/{id}/invitation| HTTP | Condition | Response body |
|---|---|---|
401 |
Missing or invalid access token | { "error": "unauthorised", ... } |
403 |
Missing write_bookings OAuth scope |
{ "error": "resource_authorisation_failure", ... } |
403 |
OAuth application not approved | { "error": "unapproved_application", ... } |
403 |
Application not authorised for the booking's lab | { "error": "resource_authorisation_failure", ... } |
403 |
Token user is not the invited user | { "error": "resource_authorisation_failure", ... } |
403 |
Token user has no invitation for this booking ID | { "error": "resource_authorisation_failure", ... } |
403 |
Accepting when invitee already has an overlapping booking on the same equipment (request_scope: current) |
{ "error": "resource_authorisation_failure", ... } |
403 |
Accepting an invitation that is already accepted (reject is still allowed on accepted invitations) | { "error": "resource_authorisation_failure", ... } |
404 |
Booking ID does not exist | { "error": "resource_not_found", ... } |
422 |
status omitted, null, or not accept/reject |
{ "error": "invalid_invitation_status", "error_description": "The status parameter must be either 'accept' or 'reject'." } |
422 |
request_scope not current, subsequent, or all |
{ "errors": [{ "attribute": "base", "message": "unknown request_scope: ..." }] } |
200 |
Bulk accept (subsequent/all) blocked by scheduling conflicts and accept_available: false |
{ "errors": [{ "attribute": "base", "message": "Cannot complete action: the invitee already has overlapping bookings..." }], "conflicting_invitations": [...], "processed_invitations": [] } |
429 |
More than 30 requests per minute per IP | HTML error page |
500 |
Unexpected server error | { "error": "internal_server_error", ... } |
POST /uploadsDoes not require lab authorisation — only a valid write_equipment token.
| HTTP | Condition | Response body |
|---|---|---|
401 |
Missing or invalid access token | { "error": "unauthorised", ... } |
403 |
Missing write_equipment OAuth scope |
{ "error": "resource_authorisation_failure", ... } |
403 |
OAuth application not approved | { "error": "unapproved_application", ... } |
422 |
See field table below | { "success": false, "message": "The upload could not be registered...", "errors": [...] } |
500 |
Unexpected server error | { "error": "internal_server_error", ... } |
422 field errors (POST /uploads)
attribute |
When |
|---|---|
filename |
Missing or blank |
content_type |
Missing or blank |
content_type |
Not a supported image type when purpose is photo (image/png, image/jpeg, image/jpg, image/svg+xml) |
content_type |
Not a supported document type when purpose is document |
byte_size |
Missing, zero, or not a positive integer |
byte_size |
Exceeds 10 MB when purpose is photo |
byte_size |
Exceeds 25 MB when purpose is document |
checksum |
Missing or blank (must be base64-encoded MD5) |
purpose |
Not photo or document |
GET /equipment/contact_people| HTTP | Condition | Response body |
|---|---|---|
401 |
Missing or invalid access token | { "error": "unauthorised", ... } |
403 |
Missing write_equipment OAuth scope |
{ "error": "resource_authorisation_failure", ... } |
403 |
OAuth application not approved | { "error": "unapproved_application", ... } |
403 |
Application not authorised for the lab (account_id) |
{ "error": "resource_authorisation_failure", ... } |
403 |
User cannot create equipment in the lab (no equipment_id; e.g. guest role) |
{ "error": "resource_authorisation_failure", ... } |
403 |
User cannot update the equipment (with equipment_id) |
{ "error": "resource_authorisation_failure", ... } |
404 |
account_id query parameter omitted |
{ "errors": ["not_found"], "error_messages": "No lab, account, or equipment was found..." } |
404 |
account_id or equipment_id does not exist |
{ "errors": ["not_found"], "error_messages": "No lab, account, or equipment was found..." } |
500 |
Unexpected server error | { "error": "internal_server_error", ... } |
POST /equipment and PATCH /equipment/{id}HTTP-level errors
| HTTP | Condition | Endpoint | Response body |
|---|---|---|---|
401 |
Missing or invalid access token | Both | { "error": "unauthorised", ... } |
403 |
Missing write_equipment OAuth scope |
Both | { "error": "resource_authorisation_failure", ... } |
403 |
OAuth application not approved | Both | { "error": "unapproved_application", ... } |
403 |
Application not authorised for the lab | Both | { "error": "resource_authorisation_failure", ... } |
403 |
User cannot create equipment in the lab | Create | { "error": "resource_authorisation_failure", ... } |
403 |
User cannot update the equipment | Update | { "error": "resource_authorisation_failure", ... } |
404 |
account_id missing or does not exist |
Create | { "errors": ["account_not_found"], ... } |
404 |
Equipment ID in URL does not exist | Update | { "errors": ["equipment_not_found"], ... } |
422 |
See field tables below | Both | { "success": false, "message": "Equipment could not be created/updated...", "errors": [...] } |
500 |
Unexpected server error | Both | { "error": "internal_server_error", ... } |
422 schema errors (create and update)
attribute |
When |
|---|---|
account_id |
Required on create but missing |
name |
Required on create but missing, or empty string |
category |
Required on create but missing, or empty string |
base |
Update request body is empty ({}) |
availability_status |
Not Always Available or Unavailable |
pricing.currency |
Not a supported currency code |
pricing.charging_type |
Not perHour or perDay |
pricing.price_details |
Wrong type (must be object with internal) |
pricing.* |
Unsupported nested field (e.g. unknown property under pricing) |
booking_rules.min_booking_time.unit |
Not perMinute, perHour, or perDay |
booking_rules.max_booking_time.unit |
Not perMinute, perHour, or perDay |
booking_rules.time_step.unit |
Not perMinute, perHour, or perDay |
booking_rules.start_step.start_step |
Not quarter, halfHour, fullHour, fullDay, or custom |
booking_rules.advance_booking_must.unit |
Not hours, days, or weeks |
booking_rules.advance_booking_can.unit |
Not hours, days, or weeks |
photos |
Wrong type on create (must be array of signed_id strings) |
photos.action |
Not extend, replace, destroy, or remove (update only) |
photos.signed_ids |
Required but missing or empty for extend/replace/remove (update) |
documents |
Same rules as photos |
documents.action |
Same rules as photos.action |
documents.signed_ids |
Same rules as photos.signed_ids |
| Any field | Wrong JSON type (e.g. string where object expected) |
<section>.<field> |
Unsupported property not in the API schema |
422 attachment errors (create and update)
attribute |
When |
|---|---|
photos |
remove action used on create (not supported) |
documents |
remove action used on create (not supported) |
photos |
Unsupported action value (not extend, replace, destroy, or remove) |
documents |
Unsupported action value |
photos.signed_ids |
Required for extend/replace/remove but missing or empty |
documents.signed_ids |
Required for extend/replace/remove but missing or empty |
photos.signed_ids |
signed_id references an attachment not on this equipment (remove on update) |
documents.signed_ids |
signed_id references an attachment not on this equipment |
photos.signed_ids |
Unknown or expired signed_id |
documents.signed_ids |
Unknown or expired signed_id |
base |
signed_id registered by a different user |
base |
signed_id registered by a different OAuth application |
base |
signed_id registered with wrong purpose (photo vs document) |
base |
File bytes not uploaded to direct_upload.url (step B incomplete) |
base |
signed_id unknown or expired — register a new upload via POST /uploads |
422 model / business-rule errors (create and update)
attribute |
When |
|---|---|
name |
Required but blank |
category |
Required but blank, or not a valid category |
subcategory |
Required for the selected category but missing |
subcategory |
Set without category, or invalid for the selected category |
manufacturer |
Required but blank, or longer than 200 characters |
model_number |
Required but blank, or longer than 200 characters |
availability_status |
Invalid value |
contact_access_role_ids |
ID is not an admin or instructor for the lab — use GET /equipment/contact_people |
group_ids |
Contains invalid group IDs for this lab |
group_ids |
Users and equipment already assigned together in another group |
charging_type |
Unsupported charging type |
currency |
Unsupported currency |
purchase_price |
Not between 0 and 99,999,999.99 |
internal_id |
Longer than 100 characters |
room |
Longer than 255 characters |
serial_number |
Longer than 255 characters |
min_booking_time |
Duration is zero |
min_booking_time |
Duration exceeds maximum booking time |
max_booking_time |
Duration is zero |
max_booking_time |
Duration exceeds 90 days |
max_booking_time |
Invalid configuration |
time_step |
Duration is zero |
time_step |
Duration exceeds maximum booking time |
start_step |
Invalid custom start step |
photos |
File exceeds 10 MB or is not an image |
documents |
File exceeds 25 MB or is unsupported type |
other_attributes |
Invalid add-ons in pricing.custom_addons |
base |
Pricing enabled but pricing.price_details.internal missing |
base |
pricing.price_details.internal is not a valid number |
base |
Invalid advance pricing configuration (overlapping time slots, etc.) |
The following write endpoints are upcoming and not yet publicly released. They are documented for early access and testing only:
GET /equipment/contact_people — list valid contact access role IDs for equipment create/updatePOST /uploads — register file uploads (step A); step B uploads bytes to direct_upload.urlPOST /equipment — create equipmentPATCH /equipment/{id} — update equipmentPOST /bookings/{id}/invitation — respond to booking invitationsThese endpoints are not generally available yet and may change without notice — including request/response schemas, validation rules, error formats, and behaviour. Do not rely on them in production until announced as stable. Contact support@calira.co for early access feedback.
Lists all accounts accessible through the integration. The application scope must include read_accounts.
Pagination: This endpoint supports pagination with a default limit of 100 records. Use page and per_page parameters to navigate through large datasets efficiently. Pagination metadata is provided in response headers (X-Total-Records, X-Current-Page, X-Per-Page).
| page | integer >= 1 Default: 1 Page number (1-based indexing, default: 1) |
| per_page | integer [ 1 .. 100 ] Default: 100 Number of records per page (1-100, default: 100, max: 100) |
[- {
- "id": 0,
- "name": "string",
- "account_type": "lab",
- "organisation": 0,
- "department": 0
}
]Get the full details of a specific account. The application scope must include read_accounts.
| id required | integer Account ID |
{- "id": 0,
- "name": "Cambridge Graphene Center",
- "account_type": "lab",
- "sector": "academia",
- "description": "string",
- "timezone": "Europe/London",
- "department": {
- "name": "string",
- "id": 0
}, - "organisation": {
- "name": "string",
- "id": 0
}, - "labs": [
- {
- "id": 0,
- "name": "string"
}
], - "departments": [
- {
- "id": 0,
- "name": "string"
}
]
}A booking made on a piece of equipment.
Write endpoints (POST /bookings/{id}/invitation) are upcoming, not yet publicly released, and currently in testing. They may change without notice.
Return lists of bookings accessible to the user, optionally scopable to the providing lab account. The application scope must include read_bookings.
Pagination: This endpoint supports pagination with a default limit of 100 records. Use page and per_page parameters to navigate through large datasets efficiently. Pagination metadata is provided in response headers (X-Total-Records, X-Current-Page, X-Per-Page).
| provider_account | number The parent lab account ID. |
| page | integer >= 1 Default: 1 Page number (1-based indexing, default: 1) |
| per_page | integer [ 1 .. 100 ] Default: 100 Number of records per page (1-100, default: 100, max: 100) |
Example response showing multiple bookings
[- {
- "id": 12345,
- "start_time": "2024-01-15T09:00:00.000Z",
- "end_time": "2024-01-15T11:00:00.000Z",
- "status": "Confirmed",
- "booking_type": "internal",
- "equipment_id": 789,
- "user_id": 456,
- "equipment": {
- "name": "Advanced Microscope"
}, - "last_public_note": null
}, - {
- "id": 12346,
- "start_time": "2024-01-16T14:00:00.000Z",
- "end_time": "2024-01-16T16:30:00.000Z",
- "status": "Pending",
- "booking_type": "external",
- "equipment_id": 790,
- "user_id": 457,
- "equipment": {
- "name": "Spectrometer"
}, - "last_public_note": {
- "message": "External booking for research collaboration"
}
}, - {
- "id": 12347,
- "start_time": "2024-01-17T08:00:00.000Z",
- "end_time": "2024-01-17T10:00:00.000Z",
- "status": "Completed",
- "booking_type": "maintenance",
- "equipment_id": 791,
- "user_id": 458,
- "equipment": {
- "name": "Centrifuge"
}, - "last_public_note": null
}
]Return the details about a particular booking as defined by the database identifier. The application scope must include read_bookings.
| id required | integer Booking ID |
Example response showing all booking information
{- "id": 12345,
- "cost": "150.00",
- "currency": "USD",
- "charging_type": "Per Hour",
- "start_time": "2024-01-15T09:00:00.000Z",
- "end_time": "2024-01-15T11:00:00.000Z",
- "requested_at": "2024-01-10T14:30:00.000Z",
- "booking_type": "internal",
- "status": "Confirmed",
- "duration": 120,
- "add_ons": "Data analysis service included",
- "requester_group": {
- "name": "Research Team Alpha"
}, - "equipment": {
- "id": 789,
- "name": "Advanced Microscope"
}, - "parent_equipment": {
- "id": 780,
- "name": "Advanced Microscope (Parent Asset)"
}, - "requester_lab": {
- "id": 101,
- "name": "Materials Science Lab"
}, - "requester_organisation": {
- "id": 201,
- "name": "University of Cambridge"
}, - "provider_lab": {
- "id": 102,
- "name": "Equipment Sharing Lab"
}, - "provider_organisation": {
- "id": 202,
- "name": "Cambridge Research Institute"
}, - "creator": {
- "id": 301,
- "name": "Dr. Jane Smith"
}, - "requester": {
- "id": 456,
- "name": "Dr. John Doe",
- "email": "john.doe@university.edu",
- "sector": "academia"
}, - "custom_form_responses": [
- {
- "response_id": 9001,
- "custom_form_version_id": 1
}, - {
- "response_id": 9002,
- "custom_form_version_id": 2
}
], - "last_public_note": {
- "message": "Please handle with care"
}, - "projects": [
- {
- "id": 501,
- "name": "Graphene Study",
- "supervisor": "Dr. Ada Lovelace",
- "funding_codes": [
- {
- "code": "EPSRC-2026-01",
- "funder": "EPSRC",
- "amount": 5000,
- "currency": "GBP"
}, - {
- "code": "INDUSTRY-2026-02",
- "funder": "Example Industries",
- "amount": 2000,
- "currency": "GBP"
}
]
}, - {
- "id": 502,
- "name": "Nano Surface Analysis",
- "supervisor": "Dr. Alan Turing",
- "funding_codes": [
- {
- "code": "EU-H2020-77",
- "funder": "European Union",
- "amount": 7500,
- "currency": "EUR"
}
]
}
]
}This write endpoint is upcoming and not yet publicly released. It is currently in testing and may not be available on all environments. Request and response shapes, validation rules, error formats, and behaviour may change without notice. Do not use in production until announced as generally available. Contact support for early access.
Accept or reject a booking invitation for the authenticated user.
When a booking is created for a group, each invited user receives a booking invitation. This endpoint lets the invited user respond on their own behalf. The booking ID in the path (/bookings/{id}/invitation) must be the ID of a booking where the current access-token user has an invitation.
| Requirement | Details |
|---|---|
| OAuth scope | write_bookings |
| Application | Must be approved and authorised for the lab that owns the booking |
| Resource owner | Must be the invited user — you cannot respond to another user's invitation |
A 403 Forbidden response is returned when the scope is missing, the application is not authorised for the lab, the token user is not the invitee, or the invitation cannot be acted on (for example, accepting when the invitee already has an overlapping booking on the same equipment).
| Field | Required | Description |
|---|---|---|
status |
Yes | accept or reject |
reject_reason |
No | Free-text reason stored when rejecting. Ignored for accept. |
request_scope |
No | Controls how many occurrences are affected for recurring bookings. Default: current |
accept_available |
No | Boolean, default false. Only relevant when accepting with a bulk request_scope |
request_scope values (recurring bookings)current (default) — only the invitation tied to this booking IDsubsequent — this booking and every later occurrence in the recurrence series where the user has an invitationall — every occurrence in the recurrence series where the user has an invitationFor non-recurring bookings, request_scope has no practical effect beyond current.
When accepting with subsequent or all, the API checks whether the invitee already has overlapping confirmed bookings on the same equipment. If conflicts exist:
accept_available: false (default): no invitations are accepted. The response still returns HTTP 200 and includes conflicting_invitations with the affected dates/times, plus an errors array explaining the conflict.accept_available: true: only non-conflicting invitations are accepted; conflicting ones are listed in conflicting_invitations.Returns the primary booking_invitation (the one for the booking ID in the path), plus:
processed_invitations — all invitations that were successfully accepted or rejected in this requestconflicting_invitations — invitations that could not be accepted due to scheduling conflicts (empty when there are no conflicts)Invitation status values in responses: pending, accepted, rejected.
See Errors (Write API) for the full catalogue.
| HTTP | Condition | Response |
|---|---|---|
401 |
Missing or invalid access token | { "error": "unauthorised", "error_description": "..." } |
403 |
Missing write_bookings scope |
{ "error": "resource_authorisation_failure", ... } |
403 |
OAuth application not approved | { "error": "unapproved_application", ... } |
403 |
Application not authorised for the booking's lab | { "error": "resource_authorisation_failure", ... } |
403 |
Token user is not the invited user | { "error": "resource_authorisation_failure", ... } |
403 |
Token user has no invitation for this booking | { "error": "resource_authorisation_failure", ... } |
403 |
Accepting when invitee has overlapping booking on same equipment (request_scope: current) |
{ "error": "resource_authorisation_failure", ... } |
403 |
Accepting an already-accepted invitation | { "error": "resource_authorisation_failure", ... } |
404 |
Booking ID does not exist | { "error": "resource_not_found", ... } |
422 |
status omitted or not accept/reject |
{ "error": "invalid_invitation_status", "error_description": "The status parameter must be either 'accept' or 'reject'." } |
422 |
request_scope not current, subsequent, or all |
{ "errors": [{ "attribute": "base", "message": "unknown request_scope: ..." }] } |
200 |
Bulk accept blocked by conflicts (accept_available: false) |
{ "errors": [{ "attribute": "base", "message": "Cannot complete action: the invitee already has overlapping bookings..." }], "conflicting_invitations": [...], "processed_invitations": [] } |
429 |
More than 30 requests per minute per IP | HTML error page |
500 |
Unexpected server error | { "error": "internal_server_error", ... } |
Accepting a confirmed booking may enqueue calendar sync jobs (for example, Outlook) for the invitee. Notifications are sent to relevant parties when an invitation is responded to.
# Accept a single invitation
POST /bookings/12345/invitation
Authorization: Bearer <token>
Content-Type: application/json
{ "status": "accept" }
# Reject with a reason
POST /bookings/12345/invitation
{ "status": "reject", "reject_reason": "Not available that day" }
# Accept all non-conflicting occurrences in a recurring series
POST /bookings/12345/invitation
{ "status": "accept", "request_scope": "all", "accept_available": true }
| id required | integer Booking ID |
| status required | string Enum: "accept" "reject" The action to take on the invitation.
|
| reject_reason | string Optional free-text reason stored when |
| request_scope | string Default: "current" Enum: "current" "subsequent" "all" For recurring bookings only. Controls which invitations in the series are affected. |
| accept_available | boolean Default: false When accepting with |
{- "status": "accept"
}{- "booking_invitation": {
- "id": 501,
- "status": "accepted",
- "booking_id": 12345,
- "reject_reason": null,
- "start_time": "2024-01-15T09:00:00.000Z",
- "end_time": "2024-01-15T11:00:00.000Z",
- "user": {
- "id": 456,
- "name": "Dr. John Doe"
}
}, - "processed_invitations": [
- {
- "id": 501,
- "status": "accepted",
- "booking_id": 12345,
- "reject_reason": null,
- "start_time": "2024-01-15T09:00:00.000Z",
- "end_time": "2024-01-15T11:00:00.000Z",
- "user": {
- "id": 456,
- "name": "Dr. John Doe"
}
}
], - "conflicting_invitations": [ ]
}Returns a submitted custom form response linked to a booking. Use this endpoint to retrieve the user-submitted answers for a booking's custom form. The application scope must include read_bookings.
Example request: GET /custom_form_responses/16
| id required | integer Custom form response ID |
{- "custom_form_response_id": 16,
- "custom_form_id": 28,
- "custom_form_version_id": 1,
- "booking_reference_id": 2045426,
- "created_by": "Demo Admin",
- "created_at": "2026-04-08T14:13:27.602Z",
- "updated_at": "2026-04-08T14:13:28.075Z",
- "form_response": [
- {
- "type": "heading",
- "text": "Project details"
}, - {
- "type": "paragraph",
- "text": "Provide the requested project information for this booking."
}, - {
- "type": "shortText",
- "text": "Project code",
- "helperText": "Internal reference code",
- "validations": {
- "isRequired": true
}, - "value": "PRJ-2026-0042"
}, - {
- "type": "longText",
- "text": "Experiment summary",
- "helperText": "Brief description of the work",
- "validations": {
- "isRequired": false
}, - "value": "Surface analysis of graphene samples."
}, - {
- "type": "number",
- "text": "Estimated samples",
- "helperText": "Number of samples to be processed",
- "validations": {
- "allowDecimals": true,
- "isRequired": true,
- "minValue": 1,
- "maxValue": 100
}, - "value": 12
}, - {
- "type": "valueUnit",
- "text": "Target thickness",
- "helperText": "Value with nanometer unit",
- "validations": {
- "allowDecimals": true,
- "isRequired": true,
- "unitType": "nm"
}, - "value": 22.5
}, - {
- "type": "dateTime",
- "text": "Requested completion date",
- "helperText": "Date only",
- "validations": {
- "isRequired": false,
- "onlyDate": true
}, - "value": "2026-04-21T23:00:00.000Z"
}, - {
- "type": "radioButton",
- "text": "Sample condition",
- "helperText": "Choose one",
- "options": [
- "Option 1",
- "Option 2"
], - "validations": {
- "isRequired": true
}, - "value": "Option 2"
}, - {
- "type": "dropdown",
- "text": "Analysis method",
- "helperText": "Select the primary method",
- "options": [
- "Option 1",
- "Option 2"
], - "validations": {
- "isRequired": true
}, - "value": "Option 1"
}, - {
- "type": "checkboxGroup",
- "text": "Safety requirements",
- "helperText": "Select all that apply",
- "options": [
- "Option 1",
- "Option 2"
], - "validations": {
- "isRequired": true
}, - "value": [
- "Option 1",
- "Option 2"
]
}, - {
- "type": "fileUpload",
- "text": "Supporting documents",
- "helperText": "Upload any supporting files",
- "validations": {
- "isRequired": false
}, - "value": [
- {
- "signed_id": "eyJfcmFpbHMiOnsiZGF0YSI6MTE3OCwicHVyIjoiYmxvYl9pZCJ9fQ==--be9e4bfde0b70ddcbc4700f1a2ac0d4417af9b93",
- "filename": "project-summary.xlsx"
}
]
}, - {
- "type": "imageUpload",
- "text": "Sample images",
- "helperText": "Upload reference images",
- "validations": {
- "isRequired": false
}, - "value": [
- {
- "signed_id": "eyJfcmFpbHMiOnsiZGF0YSI6MTE3OSwicHVyIjoiYmxvYl9pZCJ9fQ==--6109b8c4568a1687e0b95fc25955d4c52cae4b4e",
- "filename": "sample-image.png"
}
]
}
]
}A piece of equipment (e.g. microscope, spectrometer).
Write endpoints (GET /equipment/contact_people, POST /uploads, POST /equipment, PATCH /equipment/{id}) are upcoming, not yet publicly released, and currently in testing. They may change without notice.
This write endpoint is upcoming and not yet publicly released. It is currently in testing and may not be available on all environments. Request and response shapes, validation rules, error formats, and behaviour may change without notice. Do not use in production until announced as generally available. Contact support for early access.
Registers a file for direct upload before attaching it to equipment via POST /equipment or PATCH /equipment/{id}.
Equipment photos and documents are not uploaded inline with create/update requests. Instead, use this three-step direct upload flow:
| Step | Endpoint | Action |
|---|---|---|
| A | POST /uploads |
Register upload metadata and receive signed_id + direct_upload target |
| B | PUT direct_upload.url |
Upload the raw file bytes to cloud storage (not the Calira API) |
| C | POST /equipment or PATCH /equipment/{id} |
Attach the signed_id from step A |
The signed_id is scoped to the registering user and OAuth application. It must be used with the same credentials that registered it.
POST /uploads)Send file metadata (name, type, size, checksum). On success you receive:
{
"signed_id": "eyJfcmFpbHMiOnsiZGF0YSI6MTE3OSwicHVyIjoiYmxvYl9pZCJ9fQ==--6109b8c4568a1687e0b95fc25955d4c52cae4b4e",
"filename": "microscope.jpg",
"content_type": "image/jpeg",
"byte_size": 1048576,
"purpose": "photo",
"direct_upload": {
"url": "https://storage.example.com/rails/active_storage/disk/...",
"headers": {
"Content-Type": "image/jpeg",
"Content-MD5": "dGVzdCBjaGVja3N1bQ=="
}
}
}
Save signed_id for step C. Use direct_upload.url and direct_upload.headers for step B.
PUT direct_upload.url)This step does not call the Calira API. You upload the file directly to cloud storage using the pre-signed URL returned in step A.
| Requirement | Details |
|---|---|
| HTTP method | PUT (not POST) |
| URL | direct_upload.url from the step A response |
| Headers | All key/value pairs from direct_upload.headers — include them exactly as returned (typically Content-Type and Content-MD5) |
| Request body | Raw file bytes — not JSON, not multipart/form-data |
| Body size | Must match the byte_size sent in step A |
Important:
direct_upload.url is short-lived (pre-signed; expires after approximately 5 minutes by default). Complete step B promptly after step A — if the URL expires, register a new upload with POST /uploads.Content-MD5 header must match the checksum you sent in step A (base64-encoded MD5 of the file).Authorization header on this request — authentication is embedded in the pre-signed URL.signed_id in step C. If you skip step B, equipment create/update will fail with an incomplete-upload error.200 OK or 204 No Content from the storage provider (not from Calira).# Step B example — upload file bytes to storage
curl -X PUT "https://storage.example.com/rails/active_storage/disk/..." \
-H "Content-Type: image/jpeg" \
-H "Content-MD5: dGVzdCBjaGVja3N1bQ==" \
--data-binary @microscope.jpg
# Alternative: pipe file contents
curl -X PUT "https://storage.example.com/rails/active_storage/disk/..." \
-H "Content-Type: image/jpeg" \
-H "Content-MD5: dGVzdCBjaGVja3N1bQ==" \
--data-binary "@/path/to/microscope.jpg"
Reference the signed_id from step A in photos or documents when creating or updating equipment. See POST /equipment or PATCH /equipment/{id}.
| Requirement | Details |
|---|---|
| OAuth scope | write_equipment |
| Field | Required | Description |
|---|---|---|
filename |
Yes | Original file name including extension (e.g. microscope.jpg) |
content_type |
Yes | MIME type of the file. Must match the purpose restrictions below |
byte_size |
Yes | File size in bytes. Must be greater than 0 |
checksum |
Yes | Base64-encoded MD5 digest of the file bytes |
purpose |
Yes | photo or document — determines allowed types and size limits |
photo
image/png, image/jpeg, image/jpg, image/svg+xmldocument
image/jpeg, image/jpg, image/png, application/pdf, application/msword, application/vnd.ms-excel, application/vnd.oasis.opendocument.spreadsheet, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.openxmlformats-officedocument.wordprocessingml.documentOn success (201 Created), returns the signed_id to reference in equipment requests, plus direct_upload.url and direct_upload.headers for the file upload step.
See Errors (Write API) for the full catalogue. This endpoint does not require lab authorisation — only a valid write_equipment token.
| HTTP | Condition | Response |
|---|---|---|
401 |
Missing or invalid access token | { "error": "unauthorised", ... } |
403 |
Missing write_equipment scope |
{ "error": "resource_authorisation_failure", ... } |
403 |
OAuth application not approved | { "error": "unapproved_application", ... } |
422 |
Invalid upload metadata — see field table below | { "success": false, "message": "The upload could not be registered...", "errors": [...] } |
500 |
Unexpected server error | { "error": "internal_server_error", ... } |
422 field errors
attribute |
When |
|---|---|
filename |
Missing or blank |
content_type |
Missing, blank, or not allowed for the purpose |
byte_size |
Missing, zero, exceeds 10 MB (photo), or exceeds 25 MB (document) |
checksum |
Missing or blank |
purpose |
Not photo or document |
If you reference a signed_id on equipment create/update before uploading file bytes (step B), or after the signed ID expires, validation fails on the equipment request with a base error — register a new upload.
# Step A: Register upload
POST /uploads
Authorization: Bearer <token>
Content-Type: application/json
{
"filename": "microscope.jpg",
"content_type": "image/jpeg",
"byte_size": 1048576,
"checksum": "dGVzdCBjaGVja3N1bQ==",
"purpose": "photo"
}
# Response includes signed_id and direct_upload — save both for steps B and C
# Step B: Upload file bytes to direct_upload.url (NOT the Calira API)
curl -X PUT "<direct_upload.url from step A>" \
-H "Content-Type: image/jpeg" \
-H "Content-MD5: dGVzdCBjaGVja3N1bQ==" \
--data-binary @microscope.jpg
# Step C: Create equipment with the signed_id from step A
POST /equipment
Authorization: Bearer <token>
Content-Type: application/json
{
"account_id": 123,
"name": "API Microscope",
"category": "Equipment",
"photos": ["<signed_id from step A>"]
}
| filename required | string non-empty Original file name including extension. |
| content_type required | string non-empty MIME type of the file. Must match |
| byte_size required | integer >= 1 File size in bytes. Maximum 10 MB for photos, 25 MB for documents. |
| checksum required | string non-empty Base64-encoded MD5 digest of the file bytes. |
| purpose required | string Enum: "photo" "document" Determines allowed content types and size limits. |
{- "filename": "microscope.jpg",
- "content_type": "image/jpeg",
- "byte_size": 1048576,
- "checksum": "dGVzdCBjaGVja3N1bQ==",
- "purpose": "photo"
}{- "signed_id": "eyJfcmFpbHMiOnsiZGF0YSI6MTE3OSwicHVyIjoiYmxvYl9pZCJ9fQ==--6109b8c4568a1687e0b95fc25955d4c52cae4b4e",
- "filename": "microscope.jpg",
- "content_type": "image/jpeg",
- "byte_size": 1048576,
- "purpose": "photo",
- "direct_upload": {
- "headers": {
- "Content-Type": "image/jpeg",
- "Content-MD5": "dGVzdCBjaGVja3N1bQ=="
}
}
}Lists all equipment under the specified lab. Requires read_equipment scope.
Pagination: This endpoint supports pagination with a default limit of 100 records. Use page and per_page parameters to navigate through large datasets efficiently. Pagination metadata is provided in response headers (X-Total-Records, X-Current-Page, X-Per-Page).
| account | number The target lab account ID |
| exclude_parents | boolean Default: false When true, only child equipment will be returned (no parent equipment) |
| page | integer >= 1 Default: 1 Page number (1-based indexing, default: 1) |
| per_page | integer [ 1 .. 100 ] Default: 100 Number of records per page (1-100, default: 100, max: 100) |
[- {
- "id": 1,
- "name": "William Optics RedCat 71 APO",
- "description": "Apochromatic telescope",
- "room": "Room 101",
- "availability_status": "Available",
- "model": "RedCat 71",
- "manufacturer": "William Optics",
- "sub_category": {
- "id": 1,
- "name": "Audiovisual & IT"
}, - "contact_person": {
- "id": 1,
- "name": "Lisa Millar"
}, - "contact_persons": [
- {
- "id": 1,
- "name": "Lisa Millar"
}, - {
- "id": 2,
- "name": "Daniel King"
}
], - "parent_id": null,
- "parent_name": null
}, - {
- "id": 2,
- "name": "RedCat 71 Eyepiece Set",
- "description": "High-quality eyepiece collection",
- "room": "Room 101",
- "availability_status": "Available",
- "model": "RC71-EP",
- "manufacturer": "William Optics",
- "sub_category": {
- "id": 1,
- "name": "Audiovisual & IT"
}, - "contact_person": {
- "id": 1,
- "name": "Lisa Millar"
}, - "contact_persons": [
- {
- "id": 1,
- "name": "Lisa Millar"
}, - {
- "id": 2,
- "name": "Daniel King"
}
], - "parent_id": 1,
- "parent_name": "William Optics RedCat 71 APO"
}
]This write endpoint is upcoming and not yet publicly released. It is currently in testing and may not be available on all environments. Request and response shapes, validation rules, error formats, and behaviour may change without notice. Do not use in production until announced as generally available. Contact support for early access.
Creates a new piece of equipment in the specified lab account.
This endpoint creates equipment atomically in a single request. You can supply general fields, pricing, booking rules, maintenance settings, contact people, group access, labels, and file attachments in one payload.
| Requirement | Details |
|---|---|
| OAuth scope | write_equipment |
| Application | Must be approved and authorised for the target lab (account_id) |
| User | Must have permission to create equipment in that lab (typically admin or instructor) |
| Field | Description |
|---|---|
account_id |
ID of the lab account where the equipment will be created |
name |
Display name of the equipment |
category |
One of: Equipment, Parent equipment, Room/Space, Desk/Bench, Other |
Depending on the category, a subcategory may also be required (for example, Microscopy & Imaging when category is Equipment).
The request body schema defines every field with types, allowed values, and nested object structure. Expand the schema in the panel below or see these component schemas:
| Schema | Contents |
|---|---|
EquipmentMutableFields |
All top-level fields |
EquipmentPricingInput |
pricing object — currency, charging, price details, advance pricing, add-ons |
EquipmentBookingRulesInput |
booking_rules object — min/max duration, time step, start step, advance notice, business hours |
EquipmentMaintenanceInput |
maintenance object — maintenance contracts |
EquipmentAttachmentInput |
photos and documents — signed IDs (create) or action objects (update) |
| Field | Description |
|---|---|
room |
Physical location or room name |
subcategory |
Subcategory name matching the selected category |
manufacturer |
Manufacturer name |
model_number |
Model or catalogue number |
manufacturing_year |
Year of manufacture (integer or string) |
serial_number |
Serial number |
availability_status |
Always Available or Unavailable (default: Always Available) |
description |
Plain-text description |
lab_rules |
Plain-text lab-specific usage rules |
safety_features |
Plain-text safety information |
date_of_purchase |
Purchase date (ISO 8601 date string) |
internal_id |
Internal inventory identifier |
purchase_price |
Purchase price |
contact_access_role_ids |
Array of access role IDs for equipment contact people. Use GET /equipment/contact_people?account_id={id} to list valid IDs |
group_ids |
Array of group IDs that should have access |
equipment_label_ids |
Array of equipment label IDs |
photos |
Array of signed_id strings from POST /uploads with purpose: photo — see EquipmentAttachmentInput |
documents |
Array of signed_id strings from POST /uploads with purpose: document — see EquipmentAttachmentInput |
See the nested schemas above for full parameter definitions. Brief overview:
pricing → EquipmentPricingInputEnable charging, set currency (GBP, USD, …), charging_type (perHour/perDay), price_details.internal, optional advance_pricing slots, and custom_addons.
booking_rules → EquipmentBookingRulesInputEnable constraints, set min_booking_time / max_booking_time ({ unit, duration }), time_step, start_step, advance_booking_must / advance_booking_can, and optional business_hours.
maintenance → EquipmentMaintenanceInputSet maintenance_contract and maintenance_contract_ids.
Photos and documents use the three-step direct upload flow:
| Step | Action |
|---|---|
| A | POST /uploads — register metadata, receive signed_id and direct_upload |
| B | PUT direct_upload.url — upload raw file bytes to storage (see POST /uploads for full step B documentation) |
| C | Include signed_id in photos or documents on this request |
On create, photos and documents must be arrays of signed IDs (not action objects). Step B must complete before step C.
Returns 201 Created with { success: true, equipment: { ... } } containing the full equipment configuration including pricing, booking_rules, and maintenance sections.
Returns 422 Unprocessable Entity with field-level errors when validation fails. No equipment is created on validation failure.
See Errors (Write API) for the complete field-level catalogue.
| HTTP | Condition | Response |
|---|---|---|
401 |
Missing or invalid access token | { "error": "unauthorised", ... } |
403 |
Missing write_equipment scope |
{ "error": "resource_authorisation_failure", ... } |
403 |
OAuth application not approved | { "error": "unapproved_application", ... } |
403 |
Application not authorised for the lab | { "error": "resource_authorisation_failure", ... } |
403 |
User cannot create equipment in the lab | { "error": "resource_authorisation_failure", ... } |
404 |
account_id missing or does not exist |
{ "errors": ["account_not_found"], ... } |
422 |
Schema, attachment, or business-rule validation failed | { "success": false, "message": "Equipment could not be created...", "errors": [...] } |
500 |
Unexpected server error | { "error": "internal_server_error", ... } |
Common 422 causes: missing account_id/name/category; invalid subcategory, availability_status, or contact_access_role_ids; unsupported pricing/booking_rules values; invalid or incomplete photos/documents signed_id references (see upload flow).
| account_id required | integer Lab account ID where the equipment belongs. Required on create. Cannot be changed on update. |
| name required | string non-empty Display name of the equipment. Required on create. |
| room | string Physical location or room name (max 255 characters). |
| category required | string non-empty Enum: "Equipment" "Parent equipment" "Room/Space" "Desk/Bench" "Other" Equipment category. Required on create. |
| subcategory | string Subcategory name matching the selected |
| manufacturer | string Manufacturer name. Required by business rules for |
| model_number | string Model or catalogue number. Required by business rules for |
Equipment Numeric Value (number) or Equipment Numeric Value (string) (Equipment Numeric Value) Numeric value accepted as a JSON number or decimal string (e.g. | |
| serial_number | string Equipment serial number (max 255 characters). |
| availability_status | string Default: "Always Available" Enum: "Always Available" "Unavailable" Whether the equipment can be booked. |
| description | string Plain-text description. Rich-text editor formats (HTML or Quill JSON) are not accepted — send plain text only. |
| lab_rules | string Plain-text lab-specific usage rules shown to bookers. Send plain text only. |
| safety_features | string Plain-text safety information shown to bookers. Send plain text only. |
| date_of_purchase | string Purchase date as an ISO 8601 date string (e.g. |
| internal_id | string Internal inventory identifier (max 100 characters; unique per lab). |
Equipment Numeric Value (number) or Equipment Numeric Value (string) (Equipment Numeric Value) Numeric value accepted as a JSON number or decimal string (e.g. | |
| contact_access_role_ids | Array of integers Access role IDs for equipment contact people. Use |
| group_ids | Array of integers IDs of user groups that should have access to book this equipment. |
| equipment_label_ids | Array of integers IDs of equipment labels to apply. |
| maintenance_contract_ids | Array of integers IDs of maintenance contracts. May also be set inside |
Array of Equipment Attachment Input (strings) or Equipment Attachment Input (object) (Equipment Attachment Input) On create (
On update (
File bytes must be uploaded to | |
Array of Equipment Attachment Input (strings) or Equipment Attachment Input (object) (Equipment Attachment Input) On create (
On update (
File bytes must be uploaded to | |
object (Equipment Pricing Input) Pricing configuration object. Pass as the top-level When | |
object (Equipment Booking Rules Input) Booking constraint configuration. Pass as the | |
object (Equipment Maintenance Input) Maintenance contract configuration. Pass as the |
{- "account_id": 123,
- "name": "API Microscope",
- "category": "Equipment",
- "subcategory": "Microscopy & Imaging",
- "manufacturer": "Acme",
- "model_number": "X1",
- "availability_status": "Always Available"
}{- "success": true,
- "equipment": {
- "id": 1001,
- "account_id": 123,
- "name": "API Microscope",
- "category": "Equipment",
- "subcategory": "Microscopy & Imaging",
- "manufacturer": "Acme",
- "model_number": "X1",
- "availability_status": "Always Available",
- "created_at": "2024-01-15T10:00:00.000Z",
- "updated_at": "2024-01-15T10:00:00.000Z",
- "lab": {
- "id": 123,
- "name": "Materials Science Lab"
}, - "pricing": {
- "pricing": false,
- "currency": "GBP",
- "charging_type": "perHour"
}, - "booking_rules": {
- "booking_rules": false
}, - "maintenance": {
- "maintenance_contract": false
}
}
}This write endpoint is upcoming and not yet publicly released. It is currently in testing and may not be available on all environments. Request and response shapes, validation rules, error formats, and behaviour may change without notice. Do not use in production until announced as generally available. Contact support for early access.
Returns access roles that may be assigned as equipment contact people for a lab. Use the returned id values in contact_access_role_ids when creating or updating equipment.
| Scenario | Parameters |
|---|---|
| Creating new equipment | account_id only — returns lab admins and instructors |
| Updating existing equipment | account_id + equipment_id — also includes instructors assigned to that equipment |
| Requirement | Details |
|---|---|
| OAuth scope | write_equipment |
| Application | Must be approved and authorised for the lab (account_id) |
| User | Must have permission to create equipment (no equipment_id) or update the equipment (with equipment_id) |
Returns { "contact_access_roles": [ ... ] } where each item includes id, user_id, account_id, name, email, and role (admin or instructor).
See Errors (Write API) for the full catalogue.
| HTTP | Condition | Response |
|---|---|---|
401 |
Missing or invalid access token | { "error": "unauthorised", ... } |
403 |
Missing write_equipment scope |
{ "error": "resource_authorisation_failure", ... } |
403 |
OAuth application not approved | { "error": "unapproved_application", ... } |
403 |
Application not authorised for the lab | { "error": "resource_authorisation_failure", ... } |
403 |
User cannot create equipment in the lab (no equipment_id; e.g. guest) |
{ "error": "resource_authorisation_failure", ... } |
403 |
User cannot update the equipment (with equipment_id) |
{ "error": "resource_authorisation_failure", ... } |
404 |
account_id query parameter omitted |
{ "errors": ["not_found"], "error_messages": "No lab, account, or equipment was found..." } |
404 |
account_id or equipment_id does not exist |
{ "errors": ["not_found"], "error_messages": "No lab, account, or equipment was found..." } |
500 |
Unexpected server error | { "error": "internal_server_error", ... } |
| account_id required | integer Lab account ID. |
| equipment_id | integer Equipment ID when updating an existing item. Narrows the instructor list to those assigned to this equipment. |
{- "contact_access_roles": [
- {
- "id": 101,
- "user_id": 456,
- "account_id": 123,
- "name": "Dr. Jane Smith",
- "email": "jane.smith@university.edu",
- "role": "admin"
}, - {
- "id": 102,
- "user_id": 457,
- "account_id": 123,
- "name": "Dr. John Doe",
- "email": "john.doe@university.edu",
- "role": "admin"
}
]
}Returns details about a particular piece of equipment. Requires the equipment ID, and the application scope must include read_equipment.
| id required | integer Equipment ID |
{- "id": 0,
- "name": "string",
- "room": "string",
- "manufacturer": "string",
- "model": "string",
- "city": "string",
- "country": "string",
- "currency": "string",
- "charging_type": "string",
- "description": "string",
- "category": {
- "name": "string"
}, - "lab": {
- "id": 0,
- "name": "string"
}, - "contact_person": {
- "id": 0,
- "name": "string"
}, - "contact_persons": [
- {
- "id": 0,
- "name": "string"
}
], - "parent_id": 0,
- "parent_name": "string",
- "manufacturing_year": "string",
- "serial_number": "string"
}This write endpoint is upcoming and not yet publicly released. It is currently in testing and may not be available on all environments. Request and response shapes, validation rules, error formats, and behaviour may change without notice. Do not use in production until announced as generally available. Contact support for early access.
Updates an existing piece of equipment. Send only the fields you want to change — this is a partial update.
At least one updatable field must be present in the request body. An empty body returns 422 Unprocessable Entity.
Any field documented for create (except account_id) can be updated, including nested pricing, booking_rules, and maintenance sections.
See the Request body schema (EquipmentUpdateRequest → EquipmentMutableFields and nested schemas) for the full parameter reference including EquipmentAttachmentInput action objects for photos and documents.
| Requirement | Details |
|---|---|
| OAuth scope | write_equipment |
| Application | Must be approved and authorised for the lab that owns the equipment |
| User | Must have permission to update the equipment |
Unlike create, photos and documents on update use an action object (not a plain array):
| Action | signed_ids required |
Effect |
|---|---|---|
extend |
Yes | Add new attachments alongside existing ones |
replace |
Yes | Remove all existing attachments of that type and attach the given signed IDs |
remove |
Yes | Remove only the attachments matching the given signed IDs |
destroy |
No | Remove all attachments of that type |
Each signed_id must be registered via POST /uploads (step A) and file bytes uploaded via step B before use.
Returns 200 OK with { success: true, message: "Equipment was updated.", equipment: { ... } }.
Returns 422 Unprocessable Entity with field-level errors when validation fails. No partial updates are applied when validation fails.
See Errors (Write API) for the complete field-level catalogue.
| HTTP | Condition | Response |
|---|---|---|
401 |
Missing or invalid access token | { "error": "unauthorised", ... } |
403 |
Missing write_equipment scope |
{ "error": "resource_authorisation_failure", ... } |
403 |
OAuth application not approved | { "error": "unapproved_application", ... } |
403 |
Application not authorised for the lab | { "error": "resource_authorisation_failure", ... } |
403 |
User cannot update the equipment | { "error": "resource_authorisation_failure", ... } |
404 |
Equipment ID in URL does not exist | { "errors": ["equipment_not_found"], ... } |
422 |
Empty body, schema, attachment, or business-rule validation failed | { "success": false, "message": "Equipment could not be updated...", "errors": [...] } |
500 |
Unexpected server error | { "error": "internal_server_error", ... } |
Common 422 causes: empty request body; invalid field values; unsupported photos/documents action; missing signed_ids for extend/replace/remove; unknown signed_id; attachment not on equipment; incomplete upload (step B not done).
| id required | integer Equipment ID |
| account_id | integer Lab account ID where the equipment belongs. Required on create. Cannot be changed on update. |
| name | string non-empty Display name of the equipment. Required on create. |
| room | string Physical location or room name (max 255 characters). |
| category | string non-empty Enum: "Equipment" "Parent equipment" "Room/Space" "Desk/Bench" "Other" Equipment category. Required on create. |
| subcategory | string Subcategory name matching the selected |
| manufacturer | string Manufacturer name. Required by business rules for |
| model_number | string Model or catalogue number. Required by business rules for |
Equipment Numeric Value (number) or Equipment Numeric Value (string) (Equipment Numeric Value) Numeric value accepted as a JSON number or decimal string (e.g. | |
| serial_number | string Equipment serial number (max 255 characters). |
| availability_status | string Default: "Always Available" Enum: "Always Available" "Unavailable" Whether the equipment can be booked. |
| description | string Plain-text description. Rich-text editor formats (HTML or Quill JSON) are not accepted — send plain text only. |
| lab_rules | string Plain-text lab-specific usage rules shown to bookers. Send plain text only. |
| safety_features | string Plain-text safety information shown to bookers. Send plain text only. |
| date_of_purchase | string Purchase date as an ISO 8601 date string (e.g. |
| internal_id | string Internal inventory identifier (max 100 characters; unique per lab). |
Equipment Numeric Value (number) or Equipment Numeric Value (string) (Equipment Numeric Value) Numeric value accepted as a JSON number or decimal string (e.g. | |
| contact_access_role_ids | Array of integers Access role IDs for equipment contact people. Use |
| group_ids | Array of integers IDs of user groups that should have access to book this equipment. |
| equipment_label_ids | Array of integers IDs of equipment labels to apply. |
| maintenance_contract_ids | Array of integers IDs of maintenance contracts. May also be set inside |
Array of Equipment Attachment Input (strings) or Equipment Attachment Input (object) (Equipment Attachment Input) On create (
On update (
File bytes must be uploaded to | |
Array of Equipment Attachment Input (strings) or Equipment Attachment Input (object) (Equipment Attachment Input) On create (
On update (
File bytes must be uploaded to | |
object (Equipment Pricing Input) Pricing configuration object. Pass as the top-level When | |
object (Equipment Booking Rules Input) Booking constraint configuration. Pass as the | |
object (Equipment Maintenance Input) Maintenance contract configuration. Pass as the |
{- "name": "Updated Microscope",
- "manufacturer": "Updated Acme"
}{- "success": true,
- "message": "Equipment was updated.",
- "equipment": {
- "id": 1001,
- "account_id": 123,
- "name": "Updated Microscope",
- "category": "Equipment",
- "manufacturer": "Updated Acme",
- "updated_at": "2024-01-16T14:30:00.000Z",
- "lab": {
- "id": 123,
- "name": "Materials Science Lab"
}
}
}Returns bookings about a particular piece of equipment. Requires the equipment ID, and the application scope must include read_equipment.
Pagination: This endpoint supports pagination with a default limit of 100 records. Use page and per_page parameters to navigate through large datasets efficiently. Pagination metadata is provided in response headers (X-Total-Records, X-Current-Page, X-Per-Page).
| id required | integer Unique ID of the equipment |
| page | integer >= 1 Default: 1 Page number (1-based indexing, default: 1) |
| per_page | integer [ 1 .. 100 ] Default: 100 Number of records per page (1-100, default: 100, max: 100) |
[- {
- "id": 1,
- "asset_id": 1,
- "start_time": "2022-03-08T09:00:00.000+00:00",
- "end_time": "2022-03-08T11:00:00.000+00:00",
- "status": "Requested"
}
]Lists all projects under the specified account. Requires read_projects scope.
Pagination: This endpoint supports pagination with a default limit of 100 records. Use page and per_page parameters to navigate through large datasets efficiently. Pagination metadata is provided in response headers (X-Total-Records, X-Current-Page, X-Per-Page).
| account_id required | integer Account ID |
| page | integer >= 1 Default: 1 Page number (1-based indexing, default: 1) |
| per_page | integer [ 1 .. 100 ] Default: 100 Number of records per page (1-100, default: 100, max: 100) |
[- {
- "id": 0,
- "name": "string",
- "created_by": {
- "id": 0,
- "name": "string"
}, - "supervisor": "string",
- "funding_codes": [
- {
- "id": 0,
- "name": "string"
}
]
}
]Get the full details of a specific project. Requires read_projects scope.
| account_id required | integer Account ID |
| id required | integer Project ID |
{- "name": "Project 31",
- "supervisor": "Niklas",
- "additional_information": null,
- "funding_codes": [
- {
- "id": 914,
- "name": "CD60",
- "funder_name": "UKRI - AHRC",
- "funder_type": "research_council",
- "expiry_date": "2026-09-12",
- "allocated_amount": 0.2,
- "spent_amount": 0
}, - {
- "id": 915,
- "name": "CD61",
- "funder_name": "UKRI - AHRC",
- "funder_type": "research_council",
- "expiry_date": "2026-09-12",
- "allocated_amount": 0.2,
- "spent_amount": 0
}
], - "selected_users": [ ]
}Lists all groups under the specified lab. Requires read_accounts scope.
Pagination: This endpoint supports pagination with a default limit of 100 records. Use page and per_page parameters to navigate through large datasets efficiently. Pagination metadata is provided in response headers (X-Total-Records, X-Current-Page, X-Per-Page).
| account | number The target lab account ID |
| page | integer >= 1 Default: 1 Page number (1-based indexing, default: 1) |
| per_page | integer [ 1 .. 100 ] Default: 100 Number of records per page (1-100, default: 100, max: 100) |
[- {
- "id": 0,
- "name": "string"
}
]Get the full details of a specific group. The application scope must include read_accounts.
| id required | integer Group ID |
{- "id": 0,
- "name": "string",
- "equipment": [
- {
- "id": 0,
- "name": "string"
}
]
}