HTTP/JSON API exposed by cmd/server. All endpoints accept and return
application/json and use UTF-8.
- Base URL:
http://<host>:<port>(default port8080, configurable viacore.api.portinconfig/custos.yaml) - Auth: none currently enforced (deploy behind a trusted ingress / auth proxy)
- Content-Type:
application/jsonis required on every request that has a body - Unknown fields: request bodies with unknown JSON fields are rejected with
400
idfields are server-generated UUIDs when omitted from a create request.originated_idis 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 |
Liveness probe. Always returns 200 when the process is accepting connections.
Response 200
{ "status": "ok" }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
400—nameis required.409— an organization with the suppliedoriginated_idalready exists.
curl -s -X POST http://localhost:8080/organizations \
-H 'Content-Type: application/json' \
-d '{"name":"University of Example","originated_id":"ACCESS-ORG-001"}'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.
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
400—email,organization_idmissing, ororganization_iddoes not exist.409— a user with thisemailalready 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"
}'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.
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
400—title,project_pi_idmissing, or the PI user does not exist.409— a project with thisoriginated_idalready 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"
}'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.
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
400—nameis required.409— a compute cluster with thisnamealready exists.
List all compute clusters.
Response 200
[
{ "id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "name": "Delta" },
{ "id": "1d4e6a3b-7c8f-49b2-bd34-7c1f9a4e5d10", "name": "Phoenix" }
]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.
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
400—compute_cluster_id,user_id, orlocal_usernameis missing, or the referenced cluster/user does not exist.409— this user is already mapped on the given compute cluster.
Retrieve a single compute-cluster user mapping by its ID.
Errors
404— no mapping matches the supplied 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.
Remove a compute-cluster user mapping.
Response 204 — empty body.
Errors
404— no mapping matches the supplied ID.
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"
}
]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_idoruser_idis missing.404— no mapping exists for the given pair.
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"
}
]A compute allocation grants a project a budget of Service Units (SUs) on a specific compute cluster for a bounded time window.
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, orproject_id/compute_cluster_iddoes not exist.
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.
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
400—nameorresource_typeis missing.
List all compute allocation resources.
Response 200
[
{
"id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
"name": "GPU B200",
"resource_type": "GPU",
"resource_amount": 8
}
]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.
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
400—compute_allocation_resource_idmissing,resource_amount/resource_timenegative, or either the allocation or the resource does not exist.409— this resource is already attached to the allocation.
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, orresource_amount/resource_timenegative.404— no such mapping exists.
Detach a resource from a compute allocation.
Response 204 — empty body on success.
Errors
404— no such mapping exists.
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
}
]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"
}
]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.
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_idmust reference an existing resource.ratemust be ≥ 0.start_timemust be strictly beforeend_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, negativerate, or unknowncompute_allocation_resource_id.
Retrieve a rate by its ID.
Errors
404— no rate matches the supplied ID.
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"
}
]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:00ZResponse 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—atquery 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.
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, orcompute_allocation_iddoes not exist.
Retrieve a single diff by its ID.
Errors
404— no diff matches the supplied ID.
Remove a diff record. Intended for administrative cleanup; diffs are otherwise append-only.
Response 204 — empty body on success.
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"
}
]Return 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.
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, orcompute_allocation_iddoes not exist.
Retrieve a single change request by its ID.
Errors
404— no change request matches the supplied 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.
Remove a change request and (cascading) its event log.
Response 204 — empty body on success.
List every change request ever submitted against the given allocation,
ordered by timestamp ascending.
List 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.
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, orcompute_allocation_change_request_iddoes not exist.
Retrieve a single event by its ID.
Errors
404— no event matches the supplied ID.
Remove an event record. Intended for administrative cleanup; events are otherwise append-only.
Response 204 — empty body on success.
List every event recorded against the given change request, ordered by
timestamp ascending.
Return 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.
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_idanduser_idare required and must reference existing rows.membership_statusdefaults toACTIVEwhen omitted.idis generated server-side when omitted.- Per-resource SU caps for this membership are stored separately as
ComputeAllocationMembershipResourceOverriderows; see below.
Errors
400— missing required fields, or referenced allocation/user not found.409— a membership already exists for this(allocation, user)pair.
Retrieve a membership by ID.
Replace mutable fields of a membership. Fields left blank/zero in the request body fall back to the stored value (partial updates).
Update only the lifecycle status of the membership (ACTIVE, INACTIVE,
DELETED, etc.).
Request body
{ "membership_status": "INACTIVE" }Errors
400— emptymembership_status.404— no membership with the given ID.
Remove a membership.
List every membership recorded against the given allocation, ordered by
start_time ascending.
List every allocation membership held by the given user, ordered by
start_time ascending.
List 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.
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_idandcompute_allocation_resource_idare required.override_resource_amountandoverride_resource_timemust be non-negative (default0).idis generated server-side when omitted.
Errors
400— missing required fields, referenced membership/resource not found, or negativeoverride_resource_amount/override_resource_time.409— an override already exists for this(membership, resource)pair.
Retrieve an override by ID.
Replace mutable fields of an override. Fields left blank/zero in the request body fall back to the stored value (partial updates).
Remove an override.
List 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 | jqThe 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/serverA 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.