Where the API is
The workspace API reads and changes one workspace: its instruments, availability, reservations, projects, and utilization. It is part of the Core plan.
| What | Address |
|---|---|
| Base address | https://app.conductcommons.com/api/v1/workspaces/{workspace}/ |
| The workspace in the path | The workspace ID, or the last part of its address, such as your-workspace. |
| Contract | https://app.conductcommons.com/api/v1/openapi.json, as OpenAPI 3.1. It needs no key. |
The contract lists every route, field, and error. Read it for field-by-field detail.
Make a key
A core manager makes keys in the app.
- Open Settings and, under Integrations, choose Calendar links, API keys, and webhooks.
- Under API keys, type a Name for the key, such as the system that will use it.
- Tick Can also book and cancel if the key must make changes. Leave it unticked for a key that only reads.
- Choose Create key and copy the key from Your API key. It is shown only once.
Send the key with every request:
Authorization: Bearer clk_YOUR_KEY
- A key starts with
clk_and reaches only the workspace it was made in. Any other workspace in the path answers 404. - Changes made with a key run as the core manager who made it, through the same rules as the app, and send the same emails.
- A key stops working, for reads too, when the person who made it is no longer an active core manager of the workspace. Make a new key then.
- A workspace can have up to 20 keys. Revoke stops a key at once.
Scopes and routes
A key has the read scope, or read and write. The me route lists what a key may do under capabilities. Each path below follows the base address.
| Route | Scope | What it does |
|---|---|---|
GET me | read | The key, its workspace, and what the key may do. |
GET resources | read | Instruments with their booking rules, bookable hours, and rates, ordered by name. q filters by part of a name. |
GET resources/{id} | read | One instrument. |
GET resources/{id}/availability | read | Bookable hours and time already held between start and end, for up to 31 days. It does not say who holds the time or why. |
POST booking-quotes | read | Whether a booking would be accepted, and its estimate. Nothing is saved. |
GET reservations | read | Reservations ordered by start time. Filter with state, resourceId, personId, projectId, or a period of up to 366 days (start and end together). |
GET reservations/{id} | read | One reservation. |
POST reservations | write | Books time for the workspace member with the email you send. |
POST reservations/{id}/cancel | write | Cancels. Needs a reason. "scope": "series" cancels every session of a repeat booking that has not started. |
POST reservations/{id}/reschedule | write | Moves to a new startsAt and endsAt. If that time is taken, the reservation keeps its time and the answer is 409. |
POST reservations/{id}/approve | write | Confirms a request awaiting approval. A request whose hold ran out can be approved only while its time is still free. |
POST reservations/{id}/check-in | write | Starts the session, during its reserved time. |
POST reservations/{id}/check-out | write | Ends the session and sets the charge. |
GET projects | read | Projects with budget, spent, and committed amounts, ordered by name. status filters them. |
GET reports/utilization | read | Reserved and used time per instrument between start and end, for up to 366 days. |
A booking needs resourceId, email, startsAt, endsAt, and purpose, with either projectId or payingAccount. For a repeat booking, send every session under occurrences, from 2 to 60 of them, the first equal to startsAt and endsAt. All are booked, or none.
A reservation has one of these states: pending_approval, confirmed, in_use, completed, rejected, expired, cancelled, or no_show.
Rate limit
Each key may make 120 requests a minute. Successful answers carry X-RateLimit-Limit and X-RateLimit-Remaining.
Past the limit, the answer is 429 with the code RATE_LIMITED, a Retry-After: 60 header, and retryAfterSeconds in the body.
Answers and errors
- Every answer is JSON. A single record is under
data. A list is an array underdata, withnextCursorbeside it. - Every answer carries a request ID in the
X-Request-Idheader. An error repeats it asrequestIdin the body. Quote it when you ask us for help. - Times are ISO 8601 instants with an offset, such as
2026-11-03T14:00:00Z. Dates without a time, such as the first day of a project, areYYYY-MM-DD. - Amounts are decimal strings such as
"60.00", in the workspace currency named beside them. trainingRequiredon a resource is true when a person needs training to book it: the instrument asks for training and the workspace checks training.trainingChecksinmeis false when the workspace has switched training checks off.- A body may hold only the fields the route names. An unknown field is refused and named in
fields, so a request cannot set the price or the state of a reservation.
The parts of an error
| Field | What it holds |
|---|---|
error | A message that is safe to show a person. |
code | A stable name for the failure, for your code to check. |
requestId | The same ID as the X-Request-Id header. |
fields | Present when the problem belongs to named fields: each field name with the correction it needs. |
{
"error": "Check the fields named in \"fields\".",
"code": "VALIDATION_FAILED",
"requestId": "REQUEST_ID",
"fields": {
"email": "No member of this workspace has that email."
}
}Codes
| Status | Code | When |
|---|---|---|
| 401 | AUTHENTICATION_REQUIRED | No key was sent, or the key is unknown or revoked. |
| 402 | PLAN_REQUIRED | The workspace is not on the Core plan. |
| 403 | FORBIDDEN, TRAINING_REQUIRED | The key lacks the scope, or the person who made it is no longer a core manager. Or the member is not trained for the whole session. |
| 404 | NOT_FOUND | No such record in this workspace, or the path names another workspace. |
| 409 | BOOKING_CONFLICT, QUOTE_STALE, VERSION_CONFLICT, IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_IN_PROGRESS, CONFLICT | The change does not fit the current state. For example, the time is held, the price or the reservation has changed, an Idempotency-Key was reused, or the reservation is in the wrong state for the action. |
| 422 | VALIDATION_FAILED | The request cannot be accepted as written. A problem that belongs to a field is named in fields. |
| 429 | RATE_LIMITED | The key made more than 120 requests in a minute. |
| 500 | INTERNAL_ERROR | Something failed on our side. Try again, and send us the request ID if it repeats. |
Paging
The resources, reservations, and projects lists return pages. Send limit for the page size, from 1 to 200. The default is 50.
When more rows remain, nextCursor holds a value to send back unchanged as cursor. On the last page, nextCursor is null.
GET https://app.conductcommons.com/api/v1/workspaces/your-workspace/reservations?limit=100&cursor=NEXT_CURSOR
Safe retries with Idempotency-Key
When a request that makes a change times out, you cannot tell whether it went through. Send an Idempotency-Key header with the change, and you can send it again safely. POST reservations and every reservation action accept it.
- The same key with the same body returns the first answer, with the header
Idempotency-Replayed: true. Nothing changes a second time. - The same key with a different body is refused with 409
IDEMPOTENCY_KEY_REUSED. - A retry that arrives while the first request is still running gets 409
IDEMPOTENCY_IN_PROGRESS. Try again shortly. - A change that fails does not keep its key. Correct the request and send it again with the same key.
- A key is 8 to 128 letters, digits, or
._:-characters. Keys are kept for 24 hours.
Quotes
POST booking-quotes answers whether a booking would be accepted and what it would cost. It runs the booking through every rule against the live schedule, then undoes it. Nothing is saved or emailed, and the read scope is enough.
- A booking the rules accept answers
eligible: true, with the state it would start in (confirmedorpending_approval), each session, the estimate, the rate, and the setup and cleanup minutes. - A booking the rules refuse still answers 200, with
eligible: false, acode, and thereasonin words. - To book only at the quoted price, send the estimate from the quote as
expectedEstimatewhen you book. If the estimate has changed, nothing is booked and the answer is 409QUOTE_STALE.
Versions
Each reservation has a version that changes with every change to it. Send the version you last read as expectedVersion with an action. If the reservation has changed since, nothing is changed and the answer is 409 VERSION_CONFLICT. Read the reservation again, then retry.
Resources and projects carry a version too, so you can tell when one has changed.
The contract has a version of its own, in info.version of the OpenAPI document. It is 1.0.0.
Examples
Replace clk_YOUR_KEY with your key, your-workspace with the last part of your workspace address, and the IDs with your own. Use times in the future.
Check a key
curl https://app.conductcommons.com/api/v1/workspaces/your-workspace/me \ -H "Authorization: Bearer clk_YOUR_KEY"
{
"data": {
"workspace": {
"id": "WORKSPACE_ID",
"slug": "your-workspace",
"name": "Your core",
"timeZone": "America/New_York",
"currency": "USD",
"trainingChecks": true
},
"key": {
"id": "KEY_ID",
"name": "Finance dashboard",
"prefix": "clk_AbCdEfGh",
"scopes": ["read"]
},
"actsAs": {
"id": "PERSON_ID",
"name": "Core Manager",
"email": "manager@example.edu",
"role": "owner"
},
"capabilities": [
"resources.read",
"availability.read",
"booking-quotes.create",
"reservations.read",
"projects.read",
"reports.utilization.read"
],
"rateLimit": { "requests": 120, "windowSeconds": 60 }
}
}Ask for a quote
Two hours on an instrument that costs $60.00 an hour for this member and has 15 minutes of setup and 30 minutes of cleanup.
curl -X POST https://app.conductcommons.com/api/v1/workspaces/your-workspace/booking-quotes \
-H "Authorization: Bearer clk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"resourceId": "RESOURCE_ID",
"email": "researcher@example.edu",
"startsAt": "2026-11-03T14:00:00Z",
"endsAt": "2026-11-03T16:00:00Z",
"purpose": "Imaging",
"payingAccount": "Grant 1"
}'{
"data": {
"eligible": true,
"state": "confirmed",
"approvalStep": null,
"sessions": [
{
"startsAt": "2026-11-03T14:00:00.000Z",
"endsAt": "2026-11-03T16:00:00.000Z",
"estimate": "120.00"
}
],
"estimate": "120.00",
"rate": "60.00",
"rateUnit": "hour",
"rateOption": null,
"quantity": 1,
"priceGroup": "internal",
"billingBasis": "actual",
"setupMinutes": 15,
"cleanupMinutes": 30,
"overBudget": false,
"currency": "USD"
}
}If the time were already held, the answer would still be 200:
{
"data": {
"eligible": false,
"code": "BOOKING_CONFLICT",
"reason": "That time is already reserved, counting setup and cleanup time. Choose another time."
}
}Book the time
This needs a key with the write scope. The Idempotency-Key makes a retry safe, and expectedEstimate refuses the booking if the price has changed since the quote.
curl -X POST https://app.conductcommons.com/api/v1/workspaces/your-workspace/reservations \
-H "Authorization: Bearer clk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: booking-2026-11-03-imaging-01" \
-d '{
"resourceId": "RESOURCE_ID",
"email": "researcher@example.edu",
"startsAt": "2026-11-03T14:00:00Z",
"endsAt": "2026-11-03T16:00:00Z",
"purpose": "Imaging",
"payingAccount": "Grant 1",
"expectedEstimate": "120.00"
}'{
"data": {
"id": "RESERVATION_ID",
"version": "2026-10-20T15:04:05.123Z",
"state": "confirmed",
"kind": "booking",
"resource": { "id": "RESOURCE_ID", "name": "Confocal" },
"startsAt": "2026-11-03T14:00:00.000Z",
"endsAt": "2026-11-03T16:00:00.000Z",
"heldFrom": "2026-11-03T13:45:00.000Z",
"heldUntil": "2026-11-03T16:30:00.000Z",
"setupMinutes": 15,
"cleanupMinutes": 30,
"checkedInAt": null,
"checkedOutAt": null,
"checkInSource": null,
"checkOutSource": null,
"holdExpiresAt": null,
"approvalStep": null,
"person": { "id": "PERSON_ID", "name": "Ada Example", "email": "researcher@example.edu" },
"project": null,
"payingAccount": "Grant 1",
"purpose": "Imaging",
"protocolNumber": null,
"seriesId": null,
"priceGroup": "internal",
"rate": "60.00",
"rateUnit": "hour",
"rateOption": null,
"quantity": 1,
"billingBasis": "actual",
"estimate": "120.00",
"charge": null,
"fee": "0.00",
"feeReason": null,
"currency": "USD",
"createdAt": "2026-10-20T15:04:05.123Z",
"updatedAt": "2026-10-20T15:04:05.123Z"
},
"series": null
}heldFrom and heldUntil show the time held on the instrument, with setup and cleanup. charge stays null until the session is completed. For a repeat booking, data is the first session and series holds the series ID and the number of sessions.
Older routes
The routes that came before the workspace routes still work, with the same keys:
GET /api/v1/instrumentsGET /api/v1/bookingsandPOST /api/v1/bookingsPOST /api/v1/bookings/{id}/cancelGET /api/v1/projects,/api/v1/requests,/api/v1/issues, and/api/v1/invoices
They take the workspace from the key. Their errors have error and sometimes code, with no request ID. The contract lists them under Older routes.