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 HTTP_ADDR)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 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.
Path parameters: {id} — the compute allocation ID. Required body fields: compute_allocation_resource_id
Request
{ "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff" }
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" }
Errors
400 — compute_allocation_resource_id missing, or either the allocation or the resource does not exist.409 — this resource is already attached to the allocation.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.BASE=http://localhost:8080 ORG_ID=$(curl -s -X POST $BASE/organizations \ -H 'Content-Type: application/json' \ -d '{"name":"University of Example","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":"Delta"}' | 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":"GPU B200","resource_type":"GPU","resource_amount":8}' | 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\"}" | 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 # 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
export DATABASE_DSN='custos:secret@tcp(127.0.0.1:3306)/custos?parseTime=true&charset=utf8mb4' # optional export HTTP_ADDR=:8080 export DB_MAX_OPEN_CONNS=25 export DB_MAX_IDLE_CONNS=5 go run ./cmd/server
| Environment variable | Default | Purpose |
|---|---|---|
DATABASE_DSN | (required) | MySQL/MariaDB DSN. parseTime=true is mandatory. |
HTTP_ADDR | :8080 | Address the HTTP server binds to. |
DB_MAX_OPEN_CONNS | 25 | Maximum open database connections. |
DB_MAX_IDLE_CONNS | 5 | Maximum idle database connections. |
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.