HTTP/JSON API exposed by cmd/server. All endpoints accept and return application/json and use UTF-8.
http://<host>:<port> (default port 8080, configurable via core.api.port in config/custos.yaml)application/json is required on every request that has a body400id fields are server-generated UUIDs when omitted from a create request.originated_id is an optional external identifier (e.g. ACCESS Record ID) — when supplied, it must be unique within its entity type.All timestamps are RFC 3339 / ISO 8601 with timezone, e.g. 2026-05-16T12:34:56.789Z. The server emits UTC.
Errors are returned with an appropriate HTTP status code and a JSON body:
{ "error": "human-readable message" }
| Status | Meaning | Triggered by |
|---|---|---|
400 Bad Request | Malformed JSON, unknown field, missing required field, or unknown foreign-key reference | request body validation, service.ErrInvalidInput |
404 Not Found | Requested record does not exist | service.ErrNotFound |
409 Conflict | Duplicate email or duplicate originated_id | service.ErrAlreadyExists |
500 Internal Server Error | Unexpected server / database failure (driver message is logged, never returned) | any other error |
GET /healthzLiveness probe. Always returns 200 when the process is accepting connections.
Response 200
{ "status": "ok" }
POST /organizationsCreate a new organization.
Required fields: name Optional fields: id (auto-generated if omitted), originated_id
Request
{ "name": "University of Example", "originated_id": "ACCESS-ORG-001" }
Response 201
{ "id": "8c4a1b2e-7d4f-4b6a-9a0c-2f3b9d1c8e21", "originated_id": "ACCESS-ORG-001", "name": "University of Example" }
Errors
400 — name is required.409 — an organization with the supplied originated_id already exists.curl -s -X POST http://localhost:8080/organizations \ -H 'Content-Type: application/json' \ -d '{"name":"University of Example","originated_id":"ACCESS-ORG-001"}'
GET /organizations/{id}Retrieve an organization by its ID.
Response 200
{ "id": "8c4a1b2e-7d4f-4b6a-9a0c-2f3b9d1c8e21", "originated_id": "ACCESS-ORG-001", "name": "University of Example" }
Errors
404 — no organization matches the supplied ID.POST /usersCreate a new user.
Required fields: organization_id, email Optional fields: id, first_name, last_name, middle_name
The referenced organization_id must already exist; emails must be unique.
Request
{ "organization_id": "8c4a1b2e-7d4f-4b6a-9a0c-2f3b9d1c8e21", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.edu" }
Response 201
{ "id": "f0c5a4d1-2b9e-4a7c-8d31-1c5b6e3d9f02", "organization_id": "8c4a1b2e-7d4f-4b6a-9a0c-2f3b9d1c8e21", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.edu" }
Errors
400 — email, organization_id missing, or organization_id does not exist.409 — a user with this email already exists.curl -s -X POST http://localhost:8080/users \ -H 'Content-Type: application/json' \ -d '{ "organization_id":"8c4a1b2e-7d4f-4b6a-9a0c-2f3b9d1c8e21", "first_name":"Ada", "last_name":"Lovelace", "email":"ada@example.edu" }'
GET /users/{id}Retrieve a user by its ID.
Response 200
{ "id": "f0c5a4d1-2b9e-4a7c-8d31-1c5b6e3d9f02", "organization_id": "8c4a1b2e-7d4f-4b6a-9a0c-2f3b9d1c8e21", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.edu" }
Errors
404 — no user matches the supplied ID.POST /projectsCreate a new project.
Required fields: title, project_pi_id Optional fields: id, origination, originated_id, created_time (defaults to current UTC time)
The referenced project_pi_id must be an existing user. originated_id, when supplied, must be unique across projects.
Request
{ "title": "Climate Simulation 2026", "origination": "ACCESS", "originated_id": "ACCESS-PRJ-9000", "project_pi_id": "f0c5a4d1-2b9e-4a7c-8d31-1c5b6e3d9f02" }
Response 201
{ "id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1", "originated_id": "ACCESS-PRJ-9000", "title": "Climate Simulation 2026", "origination": "ACCESS", "project_pi_id": "f0c5a4d1-2b9e-4a7c-8d31-1c5b6e3d9f02", "created_time": "2026-05-16T17:21:04.512Z" }
Errors
400 — title, project_pi_id missing, or the PI user does not exist.409 — a project with this originated_id already exists.curl -s -X POST http://localhost:8080/projects \ -H 'Content-Type: application/json' \ -d '{ "title":"Climate Simulation 2026", "origination":"ACCESS", "originated_id":"ACCESS-PRJ-9000", "project_pi_id":"f0c5a4d1-2b9e-4a7c-8d31-1c5b6e3d9f02" }'
GET /projects/{id}Retrieve a project by its ID.
Response 200
{ "id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1", "originated_id": "ACCESS-PRJ-9000", "title": "Climate Simulation 2026", "origination": "ACCESS", "project_pi_id": "f0c5a4d1-2b9e-4a7c-8d31-1c5b6e3d9f02", "created_time": "2026-05-16T17:21:04.512Z" }
Errors
404 — no project matches the supplied ID.A compute cluster represents a physical or logical HPC resource (e.g. a Slurm cluster) where allocations can be provisioned.
POST /compute-clustersCreate a new compute cluster.
Required fields: name Optional fields: id (auto-generated if omitted)
name must be unique across compute clusters.
Request
{ "name": "Delta" }
Response 201
{ "id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "name": "Delta" }
Errors
400 — name is required.409 — a compute cluster with this name already exists.GET /compute-clustersList all compute clusters.
Response 200
[ { "id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "name": "Delta" }, { "id": "1d4e6a3b-7c8f-49b2-bd34-7c1f9a4e5d10", "name": "Phoenix" } ]
GET /compute-clusters/{id}Retrieve a single compute cluster by its ID.
Errors
404 — no compute cluster matches the supplied ID.A compute-cluster user maps a Custos user to their local account (local_username) on a specific compute cluster. Each (compute_cluster_id, user_id) pair is unique. The mapping is removed automatically when either the referenced compute cluster or user is deleted.
POST /compute-cluster-usersCreate a new compute-cluster user mapping.
Required fields: compute_cluster_id, user_id, local_username Optional fields: id (auto-generated if omitted)
Both compute_cluster_id and user_id must reference existing records.
Request
{ "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "user_id": "5e2c7b3a-1d8f-4d2c-bf09-83a7c4d6e210", "local_username": "jdoe" }
Response 201
{ "id": "0d72d3b1-6f1a-4a92-9c4e-1d7a4b5f9c2d", "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "user_id": "5e2c7b3a-1d8f-4d2c-bf09-83a7c4d6e210", "local_username": "jdoe" }
Errors
400 — compute_cluster_id, user_id, or local_username is missing, or the referenced cluster/user does not exist.409 — this user is already mapped on the given compute cluster.GET /compute-cluster-users/{id}Retrieve a single compute-cluster user mapping by its ID.
Errors
404 — no mapping matches the supplied ID.PUT /compute-cluster-users/{id}Replace mutable fields of an existing compute-cluster user mapping. The path {id} overrides any id in the request body.
Required fields: compute_cluster_id, user_id, local_username
Request
{ "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "user_id": "5e2c7b3a-1d8f-4d2c-bf09-83a7c4d6e210", "local_username": "jane.doe" }
Errors
400 — required fields missing.DELETE /compute-cluster-users/{id}Remove a compute-cluster user mapping.
Response 204 — empty body.
Errors
404 — no mapping matches the supplied ID.GET /compute-clusters/{id}/usersList every user mapping for the given compute cluster, ordered by local_username.
Response 200
[ { "id": "0d72d3b1-6f1a-4a92-9c4e-1d7a4b5f9c2d", "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "user_id": "5e2c7b3a-1d8f-4d2c-bf09-83a7c4d6e210", "local_username": "jdoe" } ]
GET /compute-clusters/{id}/users/{userId}Look up the single compute-cluster user mapping for the given (compute_cluster_id, user_id) pair.
Response 200
{ "id": "0d72d3b1-6f1a-4a92-9c4e-1d7a4b5f9c2d", "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "user_id": "5e2c7b3a-1d8f-4d2c-bf09-83a7c4d6e210", "local_username": "jdoe" }
Errors
400 — compute_cluster_id or user_id is missing.404 — no mapping exists for the given pair.GET /users/{id}/compute-cluster-usersList every cluster mapping held by the given Custos user, ordered by compute_cluster_id.
Response 200
[ { "id": "0d72d3b1-6f1a-4a92-9c4e-1d7a4b5f9c2d", "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "user_id": "5e2c7b3a-1d8f-4d2c-bf09-83a7c4d6e210", "local_username": "jdoe" } ]
A compute allocation grants a project a budget of Service Units (SUs) on a specific compute cluster for a bounded time window.
POST /compute-allocationsCreate a new compute allocation.
Required fields: project_id, name, compute_cluster_id Optional fields: id, status (defaults to ACTIVE), initial_su_amount, start_time, end_time
Both project_id and compute_cluster_id must reference existing records. status is one of ACTIVE, INACTIVE, DELETED.
Request
{ "project_id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1", "name": "Q2 2026 Climate Run", "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "initial_su_amount": 100000, "start_time": "2026-04-01T00:00:00Z", "end_time": "2026-06-30T23:59:59Z" }
Response 201
{ "id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", "project_id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1", "name": "Q2 2026 Climate Run", "status": "ACTIVE", "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "initial_su_amount": 100000, "start_time": "2026-04-01T00:00:00Z", "end_time": "2026-06-30T23:59:59Z" }
Errors
400 — required field missing, or project_id / compute_cluster_id does not exist.GET /compute-allocations/{id}Retrieve a compute allocation by its ID.
Errors
404 — no compute allocation matches the supplied ID.A compute allocation resource describes a hardware capability (e.g. GPU B200, CPU) that can be attached to one or more allocations.
POST /compute-allocation-resourcesCreate a new compute allocation resource.
Required fields: name, resource_type Optional fields: id, resource_amount
Request
{ "name": "GPU B200", "resource_type": "GPU", "resource_amount": 8 }
Response 201
{ "id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", "name": "GPU B200", "resource_type": "GPU", "resource_amount": 8 }
Errors
400 — name or resource_type is missing.GET /compute-allocation-resourcesList all compute allocation resources.
Response 200
[ { "id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", "name": "GPU B200", "resource_type": "GPU", "resource_amount": 8 } ]
GET /compute-allocation-resources/{id}Retrieve a compute allocation resource by its ID.
Errors
404 — no resource matches the supplied ID.A many-to-many join: an allocation can have many resources attached, and a resource can be attached to many allocations. Mappings are unique per (allocation, resource) pair, and are cascade-deleted when either parent is removed.
POST /compute-allocations/{id}/resourcesAttach an existing resource to a compute allocation, recording the amount of the resource and the wall-clock time granted to the allocation.
Path parameters: {id} — the compute allocation ID. Required body fields: compute_allocation_resource_id Optional body fields: resource_amount (int64, default 0), resource_time (int64, default 0). Both must be non-negative.
Request
{ "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", "resource_amount": 24, "resource_time": 1440 }
Response 201
{ "id": "7e1d2c3b-4a5f-4b6c-9d8e-0011223344ff", "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", "resource_amount": 24, "resource_time": 1440 }
Errors
400 — compute_allocation_resource_id missing, resource_amount / resource_time negative, or either the allocation or the resource does not exist.409 — this resource is already attached to the allocation.PUT /compute-allocations/{id}/resources/{resourceId}Update the resource_amount and resource_time recorded on an existing (allocation, resource) mapping.
Path parameters: {id} — the compute allocation ID; {resourceId} — the compute allocation resource ID. Required body fields: resource_amount, resource_time (both non-negative int64).
Request
{ "resource_amount": 48, "resource_time": 2880 }
Response 200
{ "id": "7e1d2c3b-4a5f-4b6c-9d8e-0011223344ff", "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", "resource_amount": 48, "resource_time": 2880 }
Errors
400 — either id missing, or resource_amount / resource_time negative.404 — no such mapping exists.DELETE /compute-allocations/{id}/resources/{resourceId}Detach a resource from a compute allocation.
Response 204 — empty body on success.
Errors
404 — no such mapping exists.GET /compute-allocations/{id}/resourcesList every compute allocation resource currently attached to the given compute allocation.
Response 200
[ { "id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", "name": "GPU B200", "resource_type": "GPU", "resource_amount": 8 } ]
GET /compute-allocation-resources/{id}/allocationsList every compute allocation that has the given resource attached.
Response 200
[ { "id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", "project_id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1", "name": "Q2 2026 Climate Run", "status": "ACTIVE", "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "initial_su_amount": 100000, "start_time": "2026-04-01T00:00:00Z", "end_time": "2026-06-30T23:59:59Z" } ]
A rate captures how many Service Units (SUs) are charged per unit of a compute allocation resource over a bounded time window. Multiple rates can exist for the same resource; usage at any instant is charged using the rate whose [start_time, end_time) window contains that instant.
Rates are cascade-deleted when their parent resource is deleted.
POST /compute-allocation-resource-ratesCreate a new rate for a compute allocation resource.
Required fields: compute_allocation_resource_id, rate, start_time, end_time Optional fields: id
Validation:
compute_allocation_resource_id must reference an existing resource.rate must be ≥ 0.start_time must be strictly before end_time.Request
{ "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", "rate": 2.0, "start_time": "2026-01-01T00:00:00Z", "end_time": "2026-12-31T23:59:59Z" }
Response 201
{ "id": "55aa66bb-77cc-88dd-99ee-001122334455", "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", "rate": 2.0, "start_time": "2026-01-01T00:00:00Z", "end_time": "2026-12-31T23:59:59Z" }
Errors
400 — required field missing, invalid time window, negative rate, or unknown compute_allocation_resource_id.GET /compute-allocation-resource-rates/{id}Retrieve a rate by its ID.
Errors
404 — no rate matches the supplied ID.GET /compute-allocation-resources/{id}/ratesList every rate ever defined for the given compute allocation resource, ordered by start_time ascending.
Response 200
[ { "id": "55aa66bb-77cc-88dd-99ee-001122334455", "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", "rate": 2.0, "start_time": "2026-01-01T00:00:00Z", "end_time": "2026-12-31T23:59:59Z" } ]
GET /compute-allocation-resources/{id}/rates/effectiveReturn the rate currently in effect for the given resource. By default the server uses the current time; supply ?at=<RFC 3339 timestamp> to query an arbitrary instant.
A rate is “effective” at instant t when start_time <= t < end_time. If multiple rates overlap t, the one with the most recent start_time wins.
Examples
GET /compute-allocation-resources/c0a1.../rates/effective GET /compute-allocation-resources/c0a1.../rates/effective?at=2026-05-16T12:00:00Z
Response 200
{ "id": "55aa66bb-77cc-88dd-99ee-001122334455", "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", "rate": 2.0, "start_time": "2026-01-01T00:00:00Z", "end_time": "2026-12-31T23:59:59Z" }
Errors
400 — at query parameter is not a valid RFC 3339 timestamp.404 — no rate is effective for the resource at the supplied instant.A diff is an append-only audit record of a change applied to a compute allocation — for example a usage update or a status transition. Diffs are cascade-deleted when their parent allocation is deleted.
POST /compute-allocation-diffsRecord a new diff against a compute allocation.
Required fields: compute_allocation_id, diff_type, status Optional fields: id, new_su_amount (defaults to 0), timestamp (defaults to the server's current UTC time), description
diff_type is a free-form short code such as USAGE_UPDATE or ALLOCATION_STATUS_CHANGE. status must be one of ACTIVE, INACTIVE, DELETED.
Request
{ "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", "diff_type": "USAGE_UPDATE", "new_su_amount": 90000, "status": "ACTIVE", "description": "Charged 10000 SUs for completed jobs" }
Response 201
{ "id": "44bb55cc-66dd-77ee-88ff-aabbccddeeff", "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", "diff_type": "USAGE_UPDATE", "new_su_amount": 90000, "status": "ACTIVE", "timestamp": "2026-05-16T17:42:11.918Z", "description": "Charged 10000 SUs for completed jobs" }
Errors
400 — required field missing, or compute_allocation_id does not exist.GET /compute-allocation-diffs/{id}Retrieve a single diff by its ID.
Errors
404 — no diff matches the supplied ID.DELETE /compute-allocation-diffs/{id}Remove a diff record. Intended for administrative cleanup; diffs are otherwise append-only.
Response 204 — empty body on success.
GET /compute-allocations/{id}/diffsList every diff ever recorded against the given compute allocation, ordered by timestamp ascending.
Response 200
[ { "id": "44bb55cc-66dd-77ee-88ff-aabbccddeeff", "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", "diff_type": "USAGE_UPDATE", "new_su_amount": 90000, "status": "ACTIVE", "timestamp": "2026-05-16T17:42:11.918Z", "description": "Charged 10000 SUs for completed jobs" } ]
GET /compute-allocations/{id}/diffs/latestReturn the most recent diff (highest timestamp) for the given allocation.
Errors
404 — the allocation has no diffs recorded.A change request represents a user- or admin-initiated proposal to mutate a compute allocation — e.g. asking for additional Service Units or to change its status. Each request carries a lifecycle (change_status: PENDING, APPROVED, REJECTED, etc.). Change requests are cascade-deleted when their parent allocation is deleted. Every create, update, and delete of a change request transactionally appends an entry to its event log (see below); the event log is intentionally not cascade-deleted so the audit trail survives the deletion of the parent change request.
POST /compute-allocation-change-requestsSubmit a new change request.
Required fields: compute_allocation_id, requester_id Optional fields: id, requested_su_amount, requested_status, reason, change_status (defaults to PENDING), approver_id, timestamp (defaults to the server's current UTC time)
Request
{ "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", "requested_su_amount": 120000, "requested_status": "ACTIVE", "reason": "Need more SUs for upcoming HPC runs", "requester_id": "11112222-3333-4444-5555-666677778888" }
Response 201
{ "id": "9988aabb-ccdd-eeff-0011-223344556677", "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", "requested_su_amount": 120000, "requested_status": "ACTIVE", "reason": "Need more SUs for upcoming HPC runs", "change_status": "PENDING", "requester_id": "11112222-3333-4444-5555-666677778888", "timestamp": "2026-05-16T17:42:11.918Z" }
Errors
400 — required field missing, or compute_allocation_id does not exist.GET /compute-allocation-change-requests/{id}Retrieve a single change request by its ID.
Errors
404 — no change request matches the supplied ID.PUT /compute-allocation-change-requests/{id}Replace mutable fields of a change request. Typically used by an approver to transition change_status (e.g. to APPROVED or REJECTED) and stamp approver_id. Omitted fields are preserved from the existing record.
Request
{ "change_status": "APPROVED", "approver_id": "aaaa-bbbb-cccc-dddd-eeee" }
Errors
400 — request id missing.404 — no change request matches the supplied ID.DELETE /compute-allocation-change-requests/{id}Remove a change request and (cascading) its event log.
Response 204 — empty body on success.
GET /compute-allocations/{id}/change-requestsList every change request ever submitted against the given allocation, ordered by timestamp ascending.
GET /users/{id}/change-requestsList every change request submitted by the given user, ordered by timestamp ascending.
Events are an append-only audit trail of state transitions applied to a change request — typically CREATED, APPROVED, REJECTED, UPDATED, DELETED, or arbitrary workflow markers. Create / update / delete of a change request each emit an event automatically; clients may also append custom events via the endpoint below. Events are not cascade-deleted when their parent change request is removed, so the audit trail is preserved indefinitely.
POST /compute-allocation-change-request-eventsAppend a new event to a change request.
Required fields: compute_allocation_change_request_id, event_type Optional fields: id, description, timestamp (defaults to the server's current UTC time)
Request
{ "compute_allocation_change_request_id": "9988aabb-ccdd-eeff-0011-223344556677", "event_type": "APPROVED", "description": "Change request approved by admin" }
Response 201
{ "id": "ee11ff22-3344-5566-7788-99aabbccddee", "compute_allocation_change_request_id": "9988aabb-ccdd-eeff-0011-223344556677", "event_type": "APPROVED", "description": "Change request approved by admin", "timestamp": "2026-05-16T18:00:00.000Z" }
Errors
400 — required field missing, or compute_allocation_change_request_id does not exist.GET /compute-allocation-change-request-events/{id}Retrieve a single event by its ID.
Errors
404 — no event matches the supplied ID.DELETE /compute-allocation-change-request-events/{id}Remove an event record. Intended for administrative cleanup; events are otherwise append-only.
Response 204 — empty body on success.
GET /compute-allocation-change-requests/{id}/eventsList every event recorded against the given change request, ordered by timestamp ascending.
GET /compute-allocation-change-requests/{id}/events/latestReturn the most recent event for the given change request.
Errors
404 — the change request has no events recorded.A ComputeAllocationMembership records a user's sub-allocation against a parent ComputeAllocation — i.e. how many SUs of the parent allocation the user is entitled to consume, and the time window plus lifecycle status of that grant. At most one membership can exist per (compute_allocation_id, user_id) pair (enforced by a unique key). Memberships are cascade-deleted when their parent allocation is removed.
/compute-allocation-membershipsCreate a new membership.
Request body
{ "compute_allocation_id": "alloc-123", "user_id": "user-456", "start_time": "2026-01-01T00:00:00Z", "end_time": "2026-12-31T23:59:59Z", "membership_status": "ACTIVE" }
compute_allocation_id and user_id are required and must reference existing rows.membership_status defaults to ACTIVE when omitted.id is generated server-side when omitted.ComputeAllocationMembershipResourceOverride rows; see below.Errors
400 — missing required fields, or referenced allocation/user not found.409 — a membership already exists for this (allocation, user) pair./compute-allocation-memberships/{id}Retrieve a membership by ID.
/compute-allocation-memberships/{id}Replace mutable fields of a membership. Fields left blank/zero in the request body fall back to the stored value (partial updates).
/compute-allocation-memberships/{id}/statusUpdate only the lifecycle status of the membership (ACTIVE, INACTIVE, DELETED, etc.).
Request body
{ "membership_status": "INACTIVE" }
Errors
400 — empty membership_status.404 — no membership with the given ID./compute-allocation-memberships/{id}Remove a membership.
/compute-allocations/{id}/membershipsList every membership recorded against the given allocation, ordered by start_time ascending.
/users/{id}/compute-allocation-membershipsList every allocation membership held by the given user, ordered by start_time ascending.
/compute-allocation-memberships/{id}/resource-overridesList every per-resource override recorded against the given membership.
A ComputeAllocationMembershipResourceOverride records the resource amount and wall-clock time of a specific resource (ComputeAllocationResource) that has been granted to a specific membership, overriding whatever the parent ComputeAllocationResourceMapping would otherwise provide. There can be at most one override per (compute_allocation_membership_id, compute_allocation_resource_id) pair (enforced by a unique key). Overrides are cascade-deleted when either the parent membership or the parent resource is removed.
/compute-allocation-membership-resource-overridesCreate a new override.
Request body
{ "compute_allocation_membership_id": "membership-123", "compute_allocation_resource_id": "resource-456", "override_resource_amount": 24, "override_resource_time": 1440 }
compute_allocation_membership_id and compute_allocation_resource_id are required.override_resource_amount and override_resource_time must be non-negative (default 0).id is generated server-side when omitted.Errors
400 — missing required fields, referenced membership/resource not found, or negative override_resource_amount / override_resource_time.409 — an override already exists for this (membership, resource) pair./compute-allocation-membership-resource-overrides/{id}Retrieve an override by ID.
/compute-allocation-membership-resource-overrides/{id}Replace mutable fields of an override. Fields left blank/zero in the request body fall back to the stored value (partial updates).
/compute-allocation-membership-resource-overrides/{id}Remove an override.
/compute-allocation-resources/{id}/membership-overridesList every membership override referencing the given resource.
BASE=http://localhost:8080 ORG_ID=$(curl -s -X POST $BASE/organizations \ -H 'Content-Type: application/json' \ -d '{"name":"Georgia Institute of Technology","originated_id":"ACCESS-ORG-001"}' \ | jq -r .id) USER_ID=$(curl -s -X POST $BASE/users \ -H 'Content-Type: application/json' \ -d "{\"organization_id\":\"$ORG_ID\",\"first_name\":\"Ada\",\"last_name\":\"Lovelace\",\"email\":\"ada@example.edu\"}" \ | jq -r .id) PROJ_ID=$(curl -s -X POST $BASE/projects \ -H 'Content-Type: application/json' \ -d "{\"title\":\"Climate Simulation 2026\",\"origination\":\"ACCESS\",\"originated_id\":\"ACCESS-PRJ-9000\",\"project_pi_id\":\"$USER_ID\"}" \ | jq -r .id) CLUSTER_ID=$(curl -s -X POST $BASE/compute-clusters \ -H 'Content-Type: application/json' \ -d '{"name":"nexus-dev"}' | jq -r .id) ALLOC_ID=$(curl -s -X POST $BASE/compute-allocations \ -H 'Content-Type: application/json' \ -d "{\"project_id\":\"$PROJ_ID\",\"name\":\"Q2 2026 Climate Run\",\"compute_cluster_id\":\"$CLUSTER_ID\",\"initial_su_amount\":100000}" \ | jq -r .id) RES_ID=$(curl -s -X POST $BASE/compute-allocation-resources \ -H 'Content-Type: application/json' \ -d '{"name":"debug","resource_type":"GrpTRES","resource_amount":24}' | jq -r .id) # Attach the resource to the allocation. curl -s -X POST $BASE/compute-allocations/$ALLOC_ID/resources \ -H 'Content-Type: application/json' \ -d "{\"compute_allocation_resource_id\":\"$RES_ID\",\"resource_amount\":24,\"resource_time\":1440}" | jq # Define a rate for the resource. curl -s -X POST $BASE/compute-allocation-resource-rates \ -H 'Content-Type: application/json' \ -d "{ \"compute_allocation_resource_id\":\"$RES_ID\", \"rate\":2.0, \"start_time\":\"2026-01-01T00:00:00Z\", \"end_time\":\"2026-12-31T23:59:59Z\" }" | jq # Look up the currently-effective rate. curl -s $BASE/compute-allocation-resources/$RES_ID/rates/effective | jq # Create a Custos user that will be granted access on the cluster. CLUSTER_USER_ACCT_ID=$(curl -s -X POST $BASE/users \ -H 'Content-Type: application/json' \ -d "{\"organization_id\":\"$ORG_ID\",\"first_name\":\"Dimuthu\",\"last_name\":\"Wannipurage\",\"email\":\"dimuthu@example.edu\"}" \ | jq -r .id) # Map the Custos user to a local UNIX account on the cluster. CLUSTER_USER_ID=$(curl -s -X POST $BASE/compute-cluster-users \ -H 'Content-Type: application/json' \ -d "{\"compute_cluster_id\":\"$CLUSTER_ID\",\"user_id\":\"$CLUSTER_USER_ACCT_ID\",\"local_username\":\"dimuthu\"}" \ | jq -r .id) # List all user mappings on the cluster. curl -s $BASE/compute-clusters/$CLUSTER_ID/users | jq # Grant the user a sub-allocation (membership) on the compute allocation. MEMBERSHIP_ID=$(curl -s -X POST $BASE/compute-allocation-memberships \ -H 'Content-Type: application/json' \ -d "{ \"compute_allocation_id\":\"$ALLOC_ID\", \"user_id\":\"$CLUSTER_USER_ACCT_ID\", \"start_time\":\"2026-04-01T00:00:00Z\", \"end_time\":\"2026-06-30T23:59:59Z\", \"membership_status\":\"ACTIVE\" }" | jq -r .id) # List every member of the allocation. curl -s $BASE/compute-allocations/$ALLOC_ID/memberships | jq # List every allocation this user is a member of. curl -s $BASE/users/$CLUSTER_USER_ACCT_ID/compute-allocation-memberships | jq # Grant the membership a per-resource override on the debug resource. OVERRIDE_ID=$(curl -s -X POST $BASE/compute-allocation-membership-resource-overrides \ -H 'Content-Type: application/json' \ -d "{ \"compute_allocation_membership_id\":\"$MEMBERSHIP_ID\", \"compute_allocation_resource_id\":\"$RES_ID\", \"override_resource_amount\":1, \"override_resource_time\":20 }" | jq -r .id) # Bump the override amount and time. curl -s -X PUT $BASE/compute-allocation-membership-resource-overrides/$OVERRIDE_ID \ -H 'Content-Type: application/json' \ -d '{"override_resource_amount":16,"override_resource_time":960}' | jq # List every resource override for the membership. curl -s $BASE/compute-allocation-memberships/$MEMBERSHIP_ID/resource-overrides | jq # Record a usage diff against the allocation. curl -s -X POST $BASE/compute-allocation-diffs \ -H 'Content-Type: application/json' \ -d "{ \"compute_allocation_id\":\"$ALLOC_ID\", \"diff_type\":\"USAGE_UPDATE\", \"new_su_amount\":90000, \"status\":\"ACTIVE\", \"description\":\"Charged 10000 SUs for completed jobs\" }" | jq # Inspect the diff history. curl -s $BASE/compute-allocations/$ALLOC_ID/diffs | jq curl -s $BASE/compute-allocations/$ALLOC_ID/diffs/latest | jq # Bidirectional lookups. curl -s $BASE/compute-allocations/$ALLOC_ID/resources | jq curl -s $BASE/compute-allocation-resources/$RES_ID/allocations | jq curl -s $BASE/projects/$PROJ_ID | jq
The server is configured by a YAML file (default config/custos.yaml, override with CONFIG_PATH). See CONFIG.md for the full schema.
go run ./cmd/server
A minimal config/custos.yaml:
core: database: url: "custos:secret@tcp(127.0.0.1:3306)/custos?parseTime=true&charset=utf8mb4" api: port: 8080 log_level: "info" connectors: {}
Secrets can stay out of the file via ${VAR} substitution — e.g. url: "${CUSTOS_DB_DSN}" reads the DSN from the environment at startup.
| Setting | Source | Default | Purpose |
|---|---|---|---|
core.database.url | YAML | (required) | MariaDB / MySQL DSN. parseTime=true is mandatory. |
core.api.port | YAML | 8080 | HTTP API port. |
core.log_level | YAML | info | One of debug, info, warn, error. |
DB_MAX_OPEN_CONNS | env | 25 | Maximum open database connections. |
DB_MAX_IDLE_CONNS | env | 5 | Maximum idle database connections. |
CONFIG_PATH | env | config/custos.yaml | Override the config file location. |
Migrations from internal/db/migrations/ are applied automatically on startup.
The server handles SIGINT / SIGTERM gracefully, draining in-flight requests for up to 15 seconds before exiting.