Skip to content

Latest commit

 

History

History
1472 lines (1026 loc) · 36.7 KB

File metadata and controls

1472 lines (1026 loc) · 36.7 KB

Custos API Documentation

HTTP/JSON API exposed by cmd/server. All endpoints accept and return application/json and use UTF-8.

  • Base URL: http://<host>:<port> (default port 8080, configurable via core.api.port in config/custos.yaml)
  • Auth: none currently enforced (deploy behind a trusted ingress / auth proxy)
  • Content-Type: application/json is required on every request that has a body
  • Unknown fields: request bodies with unknown JSON fields are rejected with 400

Conventions

Identifiers

  • id 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.

Timestamps

All timestamps are RFC 3339 / ISO 8601 with timezone, e.g. 2026-05-16T12:34:56.789Z. The server emits UTC.

Error format

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

Health

GET /healthz

Liveness probe. Always returns 200 when the process is accepting connections.

Response 200

{ "status": "ok" }

Organizations

POST /organizations

Create 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

  • 400name is required.
  • 409 — an organization with the supplied originated_id already exists.

Example

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.

Users

POST /users

Create 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

  • 400email, organization_id missing, or organization_id does not exist.
  • 409 — a user with this email already exists.

Example

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.

Projects

POST /projects

Create 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

  • 400title, project_pi_id missing, or the PI user does not exist.
  • 409 — a project with this originated_id already exists.

Example

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.

Compute Clusters

A compute cluster represents a physical or logical HPC resource (e.g. a Slurm cluster) where allocations can be provisioned.

POST /compute-clusters

Create 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

  • 400name is required.
  • 409 — a compute cluster with this name already exists.

GET /compute-clusters

List 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.

Compute Cluster Users

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-users

Create 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

  • 400compute_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}/users

List 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

  • 400compute_cluster_id or user_id is missing.
  • 404 — no mapping exists for the given pair.

GET /users/{id}/compute-cluster-users

List 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"
  }
]

Compute Allocations

A compute allocation grants a project a budget of Service Units (SUs) on a specific compute cluster for a bounded time window.

POST /compute-allocations

Create 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.

Compute Allocation Resources

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-resources

Create 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

  • 400name or resource_type is missing.

GET /compute-allocation-resources

List 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.

Compute Allocation ↔ Resource Mappings

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}/resources

Attach 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

  • 400compute_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}/resources

List 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}/allocations

List 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"
  }
]

Compute Allocation Resource Rates

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-rates

Create 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}/rates

List 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/effective

Return 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

  • 400at query parameter is not a valid RFC 3339 timestamp.
  • 404 — no rate is effective for the resource at the supplied instant.

Compute Allocation Diffs

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-diffs

Record 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}/diffs

List 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/latest

Return the most recent diff (highest timestamp) for the given allocation.

Errors

  • 404 — the allocation has no diffs recorded.

Compute Allocation Change Requests

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-requests

Submit 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-requests

List every change request ever submitted against the given allocation, ordered by timestamp ascending.


GET /users/{id}/change-requests

List every change request submitted by the given user, ordered by timestamp ascending.


Compute Allocation Change Request Events

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-events

Append 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}/events

List every event recorded against the given change request, ordered by timestamp ascending.


GET /compute-allocation-change-requests/{id}/events/latest

Return the most recent event for the given change request.

Errors

  • 404 — the change request has no events recorded.

Compute Allocation Memberships

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.

POST /compute-allocation-memberships

Create 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.
  • Per-resource SU caps for this membership are stored separately as 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.

GET /compute-allocation-memberships/{id}

Retrieve a membership by ID.

PUT /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).

PUT /compute-allocation-memberships/{id}/status

Update 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.

DELETE /compute-allocation-memberships/{id}

Remove a membership.

GET /compute-allocations/{id}/memberships

List every membership recorded against the given allocation, ordered by start_time ascending.

GET /users/{id}/compute-allocation-memberships

List every allocation membership held by the given user, ordered by start_time ascending.

GET /compute-allocation-memberships/{id}/resource-overrides

List every per-resource override recorded against the given membership.


Compute Allocation Membership Resource Overrides

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.

POST /compute-allocation-membership-resource-overrides

Create 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.

GET /compute-allocation-membership-resource-overrides/{id}

Retrieve an override by ID.

PUT /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).

DELETE /compute-allocation-membership-resource-overrides/{id}

Remove an override.

GET /compute-allocation-resources/{id}/membership-overrides

List every membership override referencing the given resource.


End-to-end example

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

Running the server

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.