# Register or update a project group

Registers the Forge project group Forge knows by `client_id`, or updates its name, parent and organization. Call it whenever a group is created, renamed, moved or given another organization in Forge, and once for every group that already exists, also the ones without an organization of their own: a group that is not registered ends the chain its child groups inherit through (see [List project groups](/docs/api-reference/forge-compute/list-project-groups)). Registering a deleted group's `client_id` again registers it again, with what you send.

**All three fields, every time.** Send `name`, `parent_client_id` and `organization_id` on every call, with `null` for a parent or an organization the group does not have. A rename that left one out would otherwise move the group to the top or stop billing it, so a missing field is refused with `422 VALIDATION_ERROR` rather than read as `null`.

**Choosing the organization.** `organization_id` is the organization Forge spend inside the group is billed to: one marked `selectable` in `organizations` of [List project groups](/docs/api-reference/forge-compute/list-project-groups). Any other is refused with `403 FORGE_PROJECT_GROUP_ORGANIZATION_NOT_SELECTABLE`, whose `message` says why. `null` makes the group follow its parent: it bills to its nearest registered ancestor's organization, or to none. Keeping the organization a group already has is never checked again, so a group whose organization became unusable can still be renamed or moved (its `reason` keeps saying why spend cannot be billed there); registering a deleted group again checks its organization like a new one. A change applies to new spend only: spend already billed stays on the organization it was billed to, and a [rented server](/docs/api-reference/forge-compute/create-host) stays billed to the organization it was rented in.

**Parents.** `parent_client_id` is Forge's id for the group this one sits in. A parent that is not registered yet is accepted: the group follows it as soon as it is registered, and until then `parent_id` is `null` and the chain stops there. A parent that would place the group inside itself is refused with `422 FORGE_PROJECT_GROUP_INVALID_PARENT`, and so is nesting deeper than 32 levels, counting the groups already registered below it (so registering children before their parents is checked too).

**Limit.** One person may register at most 1,000 groups. Registering one more (or registering a deleted one again) is refused with `409 FORGE_PROJECT_GROUP_LIMIT_REACHED`; updating a registered group is always allowed.

A refused request writes nothing. The answer is the whole list, exactly as [List project groups](/docs/api-reference/forge-compute/list-project-groups) returns it, so every group that follows this one shows its new organization at once. It names no organization and requires a credential that acts for a person (a bearer token, or a key belonging to a person); the group is yours alone.

#### Path Parameters

**`client_id`** string required

Forge's own id for the project group: 1 to 128 characters after surrounding spaces are removed, without control characters. An id that cannot be read is refused with `VALIDATION_ERROR` (`details.reason` `invalid_client_id`).

#### Body Parameters

**`name`** string required

The group's name, as Forge shows it: 1 to 200 characters, at least one of them visible. Surrounding whitespace is removed and every run of whitespace inside (line breaks and tabs included) becomes one space; any other control character is refused. Emoji and right-to-left text are kept as sent.

**`parent_client_id`** string or `null` required

The Forge id of the group it sits in, under the same rules as `client_id`; `null` at the top. The parent need not be registered yet.

**`organization_id`** string or `null` required

The organization its Forge spend is billed to, one marked `selectable` in [List project groups](/docs/api-reference/forge-compute/list-project-groups); `null` to follow its parent (or bill nothing to an organization when no ancestor has one). A blank string is read as `null`.

Unknown fields are refused.

## Returns

The whole list, exactly as [List project groups](/docs/api-reference/forge-compute/list-project-groups#returns) returns it: `title`, `help`, `unassigned_label`, `organizations` and `groups`, with the group just written in place.


```json
{
  "title": "Bill to",
  "help": "Forge spend inside this group's projects is billed to this organization: rented servers and Forge's AI features. Groups inside it follow it unless they choose their own.",
  "unassigned_label": "Not billed to an organization",
  "organizations": [
    {
      "id": "org_3c1e9a7b5d2f4e8a9b0c6d1e2f3a4b5c",
      "name": "Acme Engineering",
      "logo_url": "https://cdn.example.com/logos/acme.png",
      "selectable": true,
      "reason": null,
      "order": 0
    }
  ],
  "groups": [
    {
      "id": "fpgrp_4b9e2c7a1d3f4e8a9c0b6d2e1f3a5b7c",
      "client_id": "grp-clients",
      "name": "Clients",
      "parent_id": null,
      "parent_client_id": null,
      "organization": {"id": "org_3c1e9a7b5d2f4e8a9b0c6d1e2f3a4b5c", "name": "Acme Engineering"},
      "own_organization": {"id": "org_3c1e9a7b5d2f4e8a9b0c6d1e2f3a4b5c", "name": "Acme Engineering"},
      "inherited": false,
      "inherited_from": null,
      "billing_label": "Billed to Acme Engineering",
      "reason": null,
      "month_to_date_label": "CHF 12.40 this month",
      "created_at": "2026-10-09T08:12:03.418227Z",
      "updated_at": "2026-10-09T08:12:03.418227Z"
    },
    {
      "id": "fpgrp_9d3a6e1c5b7f4a2e8c0d4b6f1a3e5c7d",
      "client_id": "grp-acme-shop",
      "name": "Acme Shop",
      "parent_id": "fpgrp_4b9e2c7a1d3f4e8a9c0b6d2e1f3a5b7c",
      "parent_client_id": "grp-clients",
      "organization": {"id": "org_3c1e9a7b5d2f4e8a9b0c6d1e2f3a4b5c", "name": "Acme Engineering"},
      "own_organization": null,
      "inherited": true,
      "inherited_from": {"id": "fpgrp_4b9e2c7a1d3f4e8a9c0b6d2e1f3a5b7c", "client_id": "grp-clients", "name": "Clients"},
      "billing_label": "Billed to Acme Engineering, from Clients",
      "reason": null,
      "month_to_date_label": "CHF 0.00 this month",
      "created_at": "2026-10-09T08:12:04.902114Z",
      "updated_at": "2026-10-09T08:12:04.902114Z"
    }
  ]
}
```

## Errors

| Status | Code | When |
|  --- | --- | --- |
| 401 | `AUTHENTICATION_REQUIRED` | No valid credential. |
| 403 | `API_KEY_PERSON_REQUIRED` | An organization API key: it has no person of its own. |
| 403 | `FORGE_PROJECT_GROUP_ORGANIZATION_NOT_SELECTABLE` | `organization_id` is a new organization for the group that it may not be billed to. `message` is the reason, as a sentence to show: `You don't have permission to use Forge in this organization.`, `You are not a member of this organization.` or `This key cannot be used in this organization.`; `details.organization_id` is the organization sent. |
| 409 | `FORGE_PROJECT_GROUP_LIMIT_REACHED` | You already have 1,000 registered groups and this call would register one more. `message`: "You can register at most 1000 project groups. Delete one you no longer use and try again."; `details.limit` is `1000`. |
| 422 | `FORGE_PROJECT_GROUP_INVALID_PARENT` | `parent_client_id` would place the group inside itself. `details.reason` says how: `self` (its own parent: "A project group cannot be placed inside itself."), `cycle` (one of its own groups: "A project group cannot be placed inside one of its own groups.") or `too_deep` (nesting past 32 levels, the groups registered below it included: "Project groups cannot be nested this deeply."). `details.field` is `parent_client_id` and `details.client_id` the group. |
| 422 | `VALIDATION_ERROR` | A body of the wrong shape: a missing field (`parent_client_id` and `organization_id` must be sent, as `null` when there is none), an unknown field, a `name` that is empty, longer than 200 characters or holds control characters, a `parent_client_id` that is empty, longer than 128 characters or holds control characters, or an `organization_id` longer than 255 characters. Also a `client_id` that cannot be read (`details.reason` `invalid_client_id`). |



```bash cURL
curl -X PUT "https://api.aitronos.com/v1/forge/project-groups/grp-acme-shop" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Shop",
    "parent_client_id": "grp-clients",
    "organization_id": null
  }'
```


```python Python SDK
# pip install aitronos-sdk
#
# The Python SDK does not expose a typed `forge_project_groups` namespace yet, so
# call the endpoint directly with the same bearer token you would pass to `Aitronos(...)`.
import httpx

ACCESS_TOKEN = "your-access-token"

response = httpx.put(
    "https://api.aitronos.com/v1/forge/project-groups/grp-acme-shop",
    headers={"Authorization": f"Bearer {ACCESS_TOKEN}"},
    # All three fields, every time: null follows the parent / sits at the top.
    json={"name": "Acme Shop", "parent_client_id": "grp-clients", "organization_id": None},
)
if response.is_error:
    print(response.json()["error"]["message"])
else:
    for group in response.json()["groups"]:
        print(group["name"], "-", group["billing_label"])
```


```python Python
import os
import requests

token = os.environ["AITRONOS_TOKEN"]
response = requests.put(
    "https://api.aitronos.com/v1/forge/project-groups/grp-clients",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "name": "Clients",
        "parent_client_id": None,
        "organization_id": "org_3c1e9a7b5d2f4e8a9b0c6d1e2f3a4b5c",
    },
)
groups = {g["client_id"]: g for g in response.json()["groups"]}
print(groups["grp-clients"]["billing_label"])
```


```javascript JavaScript
const response = await fetch("https://api.aitronos.com/v1/forge/project-groups/grp-acme-shop", {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.AITRONOS_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Acme Shop", parent_client_id: "grp-clients", organization_id: null }),
});
const body = await response.json();
if (!response.ok) {
  console.log(body.error.message);
} else {
  console.log(body.groups.map((g) => `${g.name}: ${g.billing_label}`));
}
```

**Response:**

200 OK

```json
{
  "title": "Bill to",
  "help": "Forge spend inside this group's projects is billed to this organization: rented servers and Forge's AI features. Groups inside it follow it unless they choose their own.",
  "unassigned_label": "Not billed to an organization",
  "organizations": [
    {
      "id": "org_3c1e9a7b5d2f4e8a9b0c6d1e2f3a4b5c",
      "name": "Acme Engineering",
      "logo_url": "https://cdn.example.com/logos/acme.png",
      "selectable": true,
      "reason": null,
      "order": 0
    }
  ],
  "groups": [
    {
      "id": "fpgrp_4b9e2c7a1d3f4e8a9c0b6d2e1f3a5b7c",
      "client_id": "grp-clients",
      "name": "Clients",
      "parent_id": null,
      "parent_client_id": null,
      "organization": {"id": "org_3c1e9a7b5d2f4e8a9b0c6d1e2f3a4b5c", "name": "Acme Engineering"},
      "own_organization": {"id": "org_3c1e9a7b5d2f4e8a9b0c6d1e2f3a4b5c", "name": "Acme Engineering"},
      "inherited": false,
      "inherited_from": null,
      "billing_label": "Billed to Acme Engineering",
      "reason": null,
      "month_to_date_label": "CHF 12.40 this month",
      "created_at": "2026-10-09T08:12:03.418227Z",
      "updated_at": "2026-10-09T08:12:03.418227Z"
    },
    {
      "id": "fpgrp_9d3a6e1c5b7f4a2e8c0d4b6f1a3e5c7d",
      "client_id": "grp-acme-shop",
      "name": "Acme Shop",
      "parent_id": "fpgrp_4b9e2c7a1d3f4e8a9c0b6d2e1f3a5b7c",
      "parent_client_id": "grp-clients",
      "organization": {"id": "org_3c1e9a7b5d2f4e8a9b0c6d1e2f3a4b5c", "name": "Acme Engineering"},
      "own_organization": null,
      "inherited": true,
      "inherited_from": {"id": "fpgrp_4b9e2c7a1d3f4e8a9c0b6d2e1f3a5b7c", "client_id": "grp-clients", "name": "Clients"},
      "billing_label": "Billed to Acme Engineering, from Clients",
      "reason": null,
      "month_to_date_label": "CHF 0.00 this month",
      "created_at": "2026-10-09T08:12:04.902114Z",
      "updated_at": "2026-10-09T08:12:04.902114Z"
    }
  ]
}
```

403 Not selectable

```json
{
  "success": false,
  "error": {
    "code": "FORGE_PROJECT_GROUP_ORGANIZATION_NOT_SELECTABLE",
    "message": "You don't have permission to use Forge in this organization.",
    "system_message": "Forge project group organization not selectable: 'org_1a2b3c4d5e6f4a7b8c9d0e1f2a3b4c5d'",
    "type": "authorization_error",
    "status": 403,
    "details": {"organization_id": "org_1a2b3c4d5e6f4a7b8c9d0e1f2a3b4c5d"},
    "trace_id": "3f1c9a52-7d4e-4b8a-9c21-6e0d5f7a8b34",
    "timestamp": "2026-10-09T09:00:00Z"
  }
}
```

409 Limit reached

```json
{
  "success": false,
  "error": {
    "code": "FORGE_PROJECT_GROUP_LIMIT_REACHED",
    "message": "You can register at most 1000 project groups. Delete one you no longer use and try again.",
    "system_message": "Forge project group limit reached (1000)",
    "type": "client_error",
    "status": 409,
    "details": {"limit": 1000},
    "trace_id": "3f1c9a52-7d4e-4b8a-9c21-6e0d5f7a8b34",
    "timestamp": "2026-10-09T09:00:00Z"
  }
}
```

422 Invalid parent

```json
{
  "success": false,
  "error": {
    "code": "FORGE_PROJECT_GROUP_INVALID_PARENT",
    "message": "A project group cannot be placed inside one of its own groups.",
    "system_message": "Invalid Forge project group parent: cycle",
    "type": "validation_error",
    "status": 422,
    "details": {"field": "parent_client_id", "reason": "cycle", "client_id": "grp-clients"},
    "trace_id": "3f1c9a52-7d4e-4b8a-9c21-6e0d5f7a8b34",
    "timestamp": "2026-10-09T09:00:00Z"
  }
}
```

## Related Resources

- [Forge cloud compute overview](/docs/api-reference/forge-compute/introduction)
- [List billing organizations](/docs/api-reference/forge-compute/list-billing-organizations)
- [Choose the default billing organization](/docs/api-reference/forge-compute/choose-billing-organization)
- [List project groups](/docs/api-reference/forge-compute/list-project-groups)
- [Register or update a project group](/docs/api-reference/forge-compute/put-project-group)
- [Delete a project group](/docs/api-reference/forge-compute/delete-project-group)
- [List compute offerings](/docs/api-reference/forge-compute/list-offerings)
- [Get offering options](/docs/api-reference/forge-compute/get-offering-options)
- [Resolve a free size](/docs/api-reference/forge-compute/resolve-offering-size)
- [List connected accounts](/docs/api-reference/forge-compute/list-credentials)
- [Connect a cloud account](/docs/api-reference/forge-compute/put-credential)
- [Disconnect a cloud account](/docs/api-reference/forge-compute/delete-credential)
- [Create a server](/docs/api-reference/forge-compute/create-host)
- [List servers](/docs/api-reference/forge-compute/list-hosts)
- [List your servers in every organization](/docs/api-reference/forge-compute/list-my-hosts)
- [Get a server](/docs/api-reference/forge-compute/get-host)
- [Get a server's daily cost and uptime](/docs/api-reference/forge-compute/get-host-usage)
- [List a server's running intervals](/docs/api-reference/forge-compute/list-host-intervals)
- [List a server's event history](/docs/api-reference/forge-compute/list-host-events)
- [Report a setup step](/docs/api-reference/forge-compute/report-progress)
- [Report the server's host key](/docs/api-reference/forge-compute/report-host-key)
- [Mark a server ready](/docs/api-reference/forge-compute/report-ready)
- [Report activity](/docs/api-reference/forge-compute/report-activity)
- [Park a server](/docs/api-reference/forge-compute/park-host)
- [Resume a server](/docs/api-reference/forge-compute/resume-host)
- [Delete a server](/docs/api-reference/forge-compute/delete-host)
- [Revoke a server](/docs/api-reference/forge-compute/revoke-host)
- [Get compute settings](/docs/api-reference/forge-compute/get-settings)
- [Update compute settings](/docs/api-reference/forge-compute/update-settings)
- [Get spend limits](/docs/api-reference/forge-compute/get-limits)
- [Change the organization's limits](/docs/api-reference/forge-compute/update-organization-limits)
- [Set your own limit](/docs/api-reference/forge-compute/update-my-limit)
- [List member limits](/docs/api-reference/forge-compute/list-member-limits)
- [Set a member's limit](/docs/api-reference/forge-compute/update-member-limit)
- [Delete a leftover resource](/docs/api-reference/forge-compute/delete-orphan)
- [Assess a working tree's impact](/assets/assess-git-impact.a7bae1761a3dd82a227de8c76df41608e7e238562f3d2f420b7b3eccb4ee1234.23c4b6e8.md)
- [Get usage detail report](/assets/get-usage-detail-report.45870ea5b94806d0570403fa53ae031e4b48417dddf0f27bd3aa2fc29b7ba55c.49b960d1.md)