# Chainguard API v2 Tutorial

URL: https://edu.chainguard.dev/chainguard/api/api-v2-tutorial.md
Last Modified: April 2, 2026
Tags: Chainguard Console, Procedural

Tutorial with examples showing how you can use the Chainguard API v2.

The Chainguard API v2 is currently in a limited beta release for testing. It introduces cursor-based pagination, server-side ordering, consistent resource patterns, and structured error responses across all endpoints.
This guide walks through the v2 API using real curl commands.
Note: The example output in this guide was captured from a development environment. Your organization&rsquo;s resource names, UIDs, timestamps, and counts will differ. The response structure and field names are the same across all environments.
What&rsquo;s the same Authentication — same OIDC token model as v1 Authorization — same identity-based access control Scoping — same uidp.descendants_of / uidp.children_of hierarchy filters What&rsquo;s new in v2 Cursor-based pagination with page_size, page_token, total_count Server-side ordering with order_by (ascending/descending on any sortable field) Random-access pagination with skip for UI page jumping Structured errors with typed detail payloads (AIP-193) Consistent resource patterns — every resource has uid, createTime, updateTime Hydrated references — role binding responses include full identity, group, and role objects FieldMask updates — partial updates via update_mask instead of sending the full resource Available endpoints Domain Resources Operations IAM Groups, Identities, Roles, RoleBindings, IdentityProviders, AccountAssociations, GroupInvites List, Get, Create, Update, Delete Registry Repos, Tags List, Get Vulnerabilities Advisories List, Get All endpoints live under /iam/v2beta1/, /registry/v2beta1/, or /vulnerabilities/v2beta1/.
Prerequisites Get an API token and set your organization ID:
export TOKEN=$(chainctl auth token) export API=https://console-api.enforce.dev # ORG_ID is the UID of your root organization group export ORG_ID=YOUR_ORG_IDAll examples below use $TOKEN, $API, and $ORG_ID for brevity.
Beta notes Keep the following in mind as you work through this guide.
Page tokens expire after 3 days (AIP-158). If a token expires, the query restarts from the beginning — no error is returned. Rate limits are not enforced during beta. They will be introduced at GA. gRPC — all endpoints are also available via gRPC at the same host. Proto definitions are at chainguard.dev/sdk/proto/chainguard/platform/. 1. Your first v2 request List the first 3 groups in your organization:
curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&amp;page_size=3&amp;order_by=name&#34; | jq .{ &#34;groups&#34;: [ { &#34;uid&#34;: &#34;d9e2f1a0.../9f1c889071ceb6bf&#34;, &#34;name&#34;: &#34;api&#34;, &#34;description&#34;: &#34;API services and backend&#34;, &#34;resourceLimits&#34;: {}, &#34;verified&#34;: false, &#34;createTime&#34;: &#34;2026-03-27T13:20:03.456Z&#34;, &#34;updateTime&#34;: &#34;2026-03-27T13:20:03.456Z&#34; }, { &#34;uid&#34;: &#34;d9e2f1a0.../822b6e789e77ebb9&#34;, &#34;name&#34;: &#34;base-images&#34;, &#34;description&#34;: &#34;Base image maintenance&#34;, &#34;resourceLimits&#34;: {}, &#34;verified&#34;: false, &#34;createTime&#34;: &#34;2026-03-27T13:20:03.123Z&#34;, &#34;updateTime&#34;: &#34;2026-03-27T13:20:03.123Z&#34; }, { &#34;uid&#34;: &#34;d9e2f1a0.../251da0851a321620&#34;, &#34;name&#34;: &#34;ci-cd&#34;, &#34;description&#34;: &#34;CI/CD pipelines and automation&#34;, &#34;resourceLimits&#34;: {}, &#34;verified&#34;: false, &#34;createTime&#34;: &#34;2026-03-27T13:20:03.789Z&#34;, &#34;updateTime&#34;: &#34;2026-03-27T13:20:03.789Z&#34; } ], &#34;nextPageToken&#34;: &#34;CqQBV3lK...&#34;, &#34;totalCount&#34;: &#34;14&#34;, &#34;skipped&#34;: 0 }Every v2 response follows the same shape:
uid — unique resource identifier (replaces id in v1) createTime / updateTime — timestamps on every resource nextPageToken — cursor for the next page (empty when no more results) totalCount — total matching results across all pages Get a single resource New in v2: fetch a resource directly by UID. In v1, this required a List call with an ID filter.
curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2beta1/groups/$GROUP_UID&#34; | jq &#39;{uid, name, description}&#39;{ &#34;uid&#34;: &#34;d9e2f1a0.../04b8bc5bcb561945&#34;, &#34;name&#34;: &#34;engineering&#34;, &#34;description&#34;: &#34;Engineering department&#34; }Use direct UID lookups when you already know the resource identifier — they are faster than a List call with an ID filter.
Filter by name Find a specific group without knowing its UID:
curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&amp;name=platform&#34; \ | jq &#39;[.groups[] | {uid, name, description}]&#39;[ { &#34;uid&#34;: &#34;d9e2f1a0.../04b8bc5bcb561945/3af5754ef8e5dd4d&#34;, &#34;name&#34;: &#34;platform&#34;, &#34;description&#34;: &#34;Platform team — infrastructure and developer tools&#34; } ]Name filtering returns exact matches. Combine with uidp.descendants_of to scope the search to your organization.
2. Set up access for a new team A real workflow: create an org folder, add an identity, and bind a role.
Create a group curl -s -X POST -H &#34;Authorization: Bearer $TOKEN&#34; \ -H &#34;Content-Type: application/json&#34; \ &#34;$API/iam/v2beta1/groups/$ORG_ID&#34; \ -d &#39;{&#34;name&#34;: &#34;backend-team&#34;, &#34;description&#34;: &#34;Backend engineering team&#34;}&#39; | jq .{ &#34;uid&#34;: &#34;d9e2f1a0.../fb139588d99c8efe&#34;, &#34;name&#34;: &#34;backend-team&#34;, &#34;description&#34;: &#34;Backend engineering team&#34;, &#34;resourceLimits&#34;: {}, &#34;verified&#34;: false, &#34;createTime&#34;: &#34;2026-03-27T13:55:00.423Z&#34;, &#34;updateTime&#34;: &#34;2026-03-27T13:55:00.423Z&#34; } Note: The parent group goes in the URL path. The request body contains only the resource fields.
Create an identity # GROUP_UID is the uid value returned in the Create a group response above export GROUP_UID=YOUR_GROUP_UID curl -s -X POST -H &#34;Authorization: Bearer $TOKEN&#34; \ -H &#34;Content-Type: application/json&#34; \ &#34;$API/iam/v2beta1/identities/$GROUP_UID&#34; \ -d &#39;{ &#34;name&#34;: &#34;ci-bot&#34;, &#34;description&#34;: &#34;CI/CD pipeline identity&#34;, &#34;claimMatch&#34;: { &#34;issuer&#34;: &#34;https://token.actions.githubusercontent.com&#34;, &#34;subject&#34;: &#34;repo:my-org/my-repo:ref:refs/heads/main&#34; } }&#39; | jq .{ &#34;uid&#34;: &#34;d9e2f1a0.../fb139588d99c8efe/f462d354ca32ca9f&#34;, &#34;name&#34;: &#34;ci-bot&#34;, &#34;description&#34;: &#34;CI/CD pipeline identity&#34;, &#34;lastSeenTime&#34;: &#34;2026-03-27T13:55:00.783Z&#34;, &#34;createTime&#34;: &#34;2026-03-27T13:55:00.785Z&#34;, &#34;updateTime&#34;: &#34;2026-03-27T13:55:00.785Z&#34;, &#34;claimMatch&#34;: { &#34;issuer&#34;: &#34;https://token.actions.githubusercontent.com&#34;, &#34;subject&#34;: &#34;repo:my-org/my-repo:ref:refs/heads/main&#34; } }Note the identity uid in the response — you will use it in the next step when binding a role.
Bind a role First, find the viewer role:
curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2beta1/roles&#34; | jq &#39;.roles[] | select(.name == &#34;viewer&#34;) | {uid, name, description}&#39;{ &#34;uid&#34;: &#34;63921b2c44617e3f2603851537be0123af4a57d7&#34;, &#34;name&#34;: &#34;viewer&#34;, &#34;description&#34;: &#34;Viewer Role (built-in)&#34; }Then bind it:
# ROLE_UID is the uid of the viewer role, retrieved above ROLE_UID=&#34;63921b2c44617e3f2603851537be0123af4a57d7&#34; # IDENTITY_UID is the uid value returned in the Create an identity response above export IDENTITY_UID=YOUR_IDENTITY_UID curl -s -X POST -H &#34;Authorization: Bearer $TOKEN&#34; \ -H &#34;Content-Type: application/json&#34; \ &#34;$API/iam/v2beta1/roleBindings/$GROUP_UID&#34; \ -d &#34;{\&#34;identityUid\&#34;: \&#34;$IDENTITY_UID\&#34;, \&#34;roleUid\&#34;: \&#34;$ROLE_UID\&#34;}&#34; | jq .{ &#34;uid&#34;: &#34;d9e2f1a0.../fb139588.../9b822036a7075d75&#34;, &#34;identity&#34;: { &#34;uid&#34;: &#34;d9e2f1a0.../fb139588.../f462d354ca32ca9f&#34;, &#34;name&#34;: &#34;ci-bot&#34;, &#34;description&#34;: &#34;CI/CD pipeline identity&#34;, &#34;subject&#34;: &#34;repo:my-org/my-repo:ref:refs/heads/main&#34;, &#34;issuer&#34;: &#34;https://token.actions.githubusercontent.com&#34; }, &#34;group&#34;: { &#34;uid&#34;: &#34;d9e2f1a0.../fb139588d99c8efe&#34;, &#34;name&#34;: &#34;backend-team&#34;, &#34;description&#34;: &#34;Backend engineering team&#34; }, &#34;role&#34;: { &#34;uid&#34;: &#34;63921b2c44617e3f2603851537be0123af4a57d7&#34;, &#34;name&#34;: &#34;viewer&#34;, &#34;description&#34;: &#34;Viewer Role (built-in)&#34; }, &#34;createTime&#34;: &#34;2026-03-27T13:55:01.475Z&#34; }The response includes fully hydrated identity, group, and role objects — no need for follow-up lookups.
3. Pagination Every List endpoint supports cursor-based pagination with consistent parameters.
Basic pagination curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&amp;page_size=5&#34; \ | jq &#39;{totalCount, groups: [.groups[].name], nextPageToken: .nextPageToken[:20]}&#39;{ &#34;totalCount&#34;: &#34;14&#34;, &#34;groups&#34;: [&#34;api&#34;, &#34;base-images&#34;, &#34;ci-cd&#34;, &#34;containers&#34;, &#34;engineering&#34;], &#34;nextPageToken&#34;: &#34;CqQBV3lKbE16Z3dPVE0y&#34; }Follow the cursor for the next page:
curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&amp;page_size=5&amp;page_token=CqQBV3lK...&#34; \ | jq &#39;{groups: [.groups[].name]}&#39;{ &#34;groups&#34;: [&#34;incident-response&#34;, &#34;platform&#34;, &#34;production&#34;, &#34;registry-ops&#34;, &#34;root&#34;] }When nextPageToken is absent from the response, you have reached the last page.
Server-side ordering Sort by name:
curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&amp;page_size=5&amp;order_by=name&#34; \ | jq &#39;[.groups[].name]&#39;[&#34;api&#34;, &#34;base-images&#34;, &#34;ci-cd&#34;, &#34;containers&#34;, &#34;engineering&#34;]Reverse the order:
curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&amp;page_size=5&amp;order_by=name%20desc&#34; \ | jq &#39;[.groups[].name]&#39;[&#34;vuln-scanning&#34;, &#34;staging&#34;, &#34;security&#34;, &#34;sandbox&#34;, &#34;root&#34;]Sort by creation time (newest first):
curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&amp;page_size=5&amp;order_by=created_at%20desc&#34; \ | jq &#39;[.groups[] | {name, createTime}]&#39;[ {&#34;name&#34;: &#34;sandbox&#34;, &#34;createTime&#34;: &#34;2026-03-27T13:20:05.488Z&#34;}, {&#34;name&#34;: &#34;production&#34;, &#34;createTime&#34;: &#34;2026-03-27T13:20:05.135Z&#34;}, {&#34;name&#34;: &#34;staging&#34;, &#34;createTime&#34;: &#34;2026-03-27T13:20:04.814Z&#34;}, {&#34;name&#34;: &#34;incident-response&#34;, &#34;createTime&#34;: &#34;2026-03-27T13:20:04.257Z&#34;}, {&#34;name&#34;: &#34;vuln-scanning&#34;, &#34;createTime&#34;: &#34;2026-03-27T13:20:03.915Z&#34;} ]Pagination and ordering combine: pages maintain sort order across cursors.
Random-access with skip Jump directly to page 3 (skip the first 10 results):
curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&amp;page_size=5&amp;order_by=name&amp;skip=10&#34; \ | jq &#39;{skipped: .skipped, groups: [.groups[].name]}&#39;{ &#34;skipped&#34;: 10, &#34;groups&#34;: [&#34;sandbox&#34;, &#34;security&#34;, &#34;staging&#34;, &#34;vuln-scanning&#34;] }The skipped field in the response confirms how many results were skipped, useful for building UI page controls.
Pagination parameters Parameter Description page_size Number of results per page (default 50, max 200) page_token Opaque cursor from previous response&rsquo;s nextPageToken order_by Sort field and direction, e.g. name, created_at desc skip Number of results to skip (for random-access / UI page jumping) 4. Querying the registry List repos scoped to your organization:
curl -s -H &#34;Authorization: Bearer $TOKEN&#34; \ &#34;$API/registry/v2beta1/repos?uidp.descendants_of=$ORG_ID&amp;page_size=3&#34; \ | jq &#39;[.repos[] | {uid, name, createTime}]&#39;[ {&#34;uid&#34;: &#34;d9e2f1a0.../06626efd8c6b3fb7&#34;, &#34;name&#34;: &#34;nginx&#34;, &#34;createTime&#34;: &#34;2026-01-28T12:54:21.189Z&#34;}, {&#34;uid&#34;: &#34;d9e2f1a0.../0ed18f0f929f4c60&#34;, &#34;name&#34;: &#34;python&#34;, &#34;createTime&#34;: &#34;2026-01-23T14:54:42.774Z&#34;}, {&#34;uid&#34;: &#34;d9e2f1a0.../12b4208b23740c37&#34;, &#34;name&#34;: &#34;static&#34;, &#34;createTime&#34;: &#34;2026-01-23T14:54:39.021Z&#34;} ]Same pagination and ordering parameters work on all List endpoints.
5. Structured errors API v2 returns structured error responses with machine-parseable codes and details.
Validation error curl -s -X POST -H &#34;Authorization: Bearer $TOKEN&#34; \ -H &#34;Content-Type: application/json&#34; \ &#34;$API/iam/v2beta1/groups/$ORG_ID&#34; \ -d &#39;{}&#39; | jq .{ &#34;code&#34;: 3, &#34;message&#34;: &#34;Invalid argument: name: name must match \&#34;^[a-z0-9 ._-]{1,}$\&#34;&#34;, &#34;details&#34;: [ { &#34;@type&#34;: &#34;type.googleapis.com/google.rpc.ErrorInfo&#34;, &#34;reason&#34;: &#34;INVALID_ARGUMENT&#34;, &#34;domain&#34;: &#34;iam.chainguard.dev&#34; }, { &#34;@type&#34;: &#34;type.googleapis.com/google.rpc.BadRequest&#34;, &#34;fieldViolations&#34;: [ { &#34;field&#34;: &#34;name&#34;, &#34;description&#34;: &#34;name must match \&#34;^[a-z0-9 ._-]{1,}$\&#34;&#34; } ] } ] }The fieldViolations array identifies exactly which fields failed validation and why.
Precondition failure Attempting to delete a group that still contains child resources returns a precondition failure:
{ &#34;code&#34;: 9, &#34;message&#34;: &#34;Precondition failed: cannot delete group with child repos&#34;, &#34;details&#34;: [ { &#34;@type&#34;: &#34;type.googleapis.com/google.rpc.ErrorInfo&#34;, &#34;reason&#34;: &#34;FAILED_PRECONDITION&#34;, &#34;domain&#34;: &#34;iam.chainguard.dev&#34; }, { &#34;@type&#34;: &#34;type.googleapis.com/google.rpc.PreconditionFailure&#34;, &#34;violations&#34;: [ { &#34;type&#34;: &#34;RESOURCE_NOT_EMPTY&#34;, &#34;description&#34;: &#34;cannot delete group with child repos&#34; } ] } ] }Error responses follow Google AIP-193 with typed detail payloads you can switch on programmatically.
6. Partial updates with FieldMask Update specific fields without sending the full resource. Only the fields listed in updateMask are changed:
curl -s -X PATCH -H &#34;Authorization: Bearer $TOKEN&#34; \ -H &#34;Content-Type: application/json&#34; \ &#34;$API/iam/v2beta1/groups/$GROUP_UID&#34; \ -d &#39;{ &#34;description&#34;: &#34;Updated description — only this field changes&#34; }&#39; | jq &#39;{uid, name, description}&#39;{ &#34;uid&#34;: &#34;d9e2f1a0.../fb139588d99c8efe&#34;, &#34;name&#34;: &#34;backend-team&#34;, &#34;description&#34;: &#34;Updated description — only this field changes&#34; }The name field was not in the request body, so it&rsquo;s unchanged. In v1, updates required sending the entire resource — any omitted field would be reset to its zero value.
To be explicit about which fields to update, pass updateMask:
curl -s -X PATCH -H &#34;Authorization: Bearer $TOKEN&#34; \ -H &#34;Content-Type: application/json&#34; \ &#34;$API/iam/v2beta1/groups/$GROUP_UID?updateMask=description&#34; \ -d &#39;{ &#34;description&#34;: &#34;Only this field is updated&#34;, &#34;name&#34;: &#34;this-is-ignored&#34; }&#39; | jq &#39;{uid, name, description}&#39;{ &#34;uid&#34;: &#34;d9e2f1a0.../fb139588d99c8efe&#34;, &#34;name&#34;: &#34;backend-team&#34;, &#34;description&#34;: &#34;Only this field is updated&#34; }The name in the body is ignored because updateMask only includes description.
Migration from v1 v2 is additive — v1 endpoints remain available. You can migrate at your own pace:
Replace /iam/v1/ with /iam/v2beta1/ in your API calls Update field names: id → uid, createdAt → createTime, updatedAt → updateTime Add pagination handling for List endpoints (or set page_size high for small result sets) v1 will have a deprecation window after v2 reaches GA Cleanup Delete resources you created during this walkthrough:
# Delete in reverse order: role binding, identity, group # BINDING_UID is the uid value returned in the Bind a role response above curl -s -X DELETE -H &#34;Authorization: Bearer $TOKEN&#34; &#34;$API/iam/v2beta1/roleBindings/$BINDING_UID&#34; curl -s -X DELETE -H &#34;Authorization: Bearer $TOKEN&#34; &#34;$API/iam/v2beta1/identities/$IDENTITY_UID&#34; curl -s -X DELETE -H &#34;Authorization: Bearer $TOKEN&#34; &#34;$API/iam/v2beta1/groups/$GROUP_UID&#34;Each DELETE returns an empty response body on success.

