Skip to content

Manage the registry

Every registry route sits under /api/v1/tenants/{tenant}/registries. A tenant has one registry and it is always named registry, so {name} is always registry. Any other name is 404. A cluster connection is named after its cluster, so {binding} is the cluster’s name.

Method Path What it does Tenant role
GET /registries List the registry (at most one) readonly
POST /registries Turn on the registry admin
GET /registries/registry Get the registry readonly
PATCH /registries/registry Change the storage limit or deletion protection admin
DELETE /registries/registry Delete the registry and every image in it admin
GET /registries/registry/bindings List cluster connections readonly
POST /registries/registry/bindings Connect a cluster admin
GET /registries/registry/bindings/{cluster} Get a cluster connection readonly
PATCH /registries/registry/bindings/{cluster} Change its push namespaces admin
DELETE /registries/registry/bindings/{cluster} Disconnect a cluster admin
POST /registries/registry/bindings/{cluster}/rotate Rotate its credentials admin
GET /registries/registry/repositories List repositories readonly
GET /registries/registry/repositories/{repository} Get a repository readonly
DELETE /registries/registry/repositories/{repository} Delete a repository admin
GET /registries/registry/repositories/{repository}/images List images readonly
GET /registries/registry/repositories/{repository}/images/{reference} Get an image by digest or tag readonly
DELETE /registries/registry/repositories/{repository}/images/{digest} Delete an image admin
DELETE /registries/registry/repositories/{repository}/tags/{tag} Delete a tag admin
GET /registries/registry/tokens List access tokens and login sessions readonly
POST /registries/registry/tokens Create an access token admin
GET /registries/registry/tokens/{id} Get an access token or login session readonly
DELETE /registries/registry/tokens/{id} Revoke an access token or login session admin, or a session’s own creator
POST /registries/registry/sessions Create a login session any member, signed in

The API reference lists these routes under registries. The examples use:

Terminal window
export KUPE_TENANT="<tenant>"
export API="https://api.kupe.cloud/api/v1/tenants/$KUPE_TENANT/registries"
Terminal window
curl -X POST \
-H "Authorization: Bearer $KUPE_API_KEY" \
-H "Content-Type: application/json" \
"$API" \
-d '{"storageLimit": "10Gi"}'
Field Required Mutable Description
name No No "registry", the default and the only name accepted.
storageLimit No Yes A limit below your plan’s cap, in Mi, Gi or Ti, such as "10Gi". Omit it to take the plan’s cap.
deletionProtection No Yes Default true. Must be false before the registry can be deleted.

201 Created returns the registry. It is provisioned asynchronously: poll GET $API/registry until status.phase is Running.

{
"name": "registry",
"storageLimit": "10Gi",
"deletionProtection": true,
"status": {
"observedGeneration": 1,
"phase": "Running",
"endpoint": "registry.kupe.cloud/acme",
"storageUsedBytes": 1073741824,
"storageLimitBytes": 10737418240,
"repositoryCount": 3,
"artifactCount": 12,
"usageObservedAt": "2026-10-21T10:05:00Z",
"conditions": []
},
"generation": 1,
"resourceVersion": "582019",
"createdAt": "2026-10-21T10:00:00Z"
}

status.endpoint is the image path prefix to push to. storageLimitBytes is the limit in force, -1 for unlimited, and absent until a limit has been applied.

Change the storage limit or deletion protection

Section titled “Change the storage limit or deletion protection”

PATCH uses JSON merge-patch semantics and takes storageLimit, deletionProtection, or both. "storageLimit": "" removes your limit, so the plan’s cap applies. Send If-Match with the ETag from a GET to avoid overwriting someone else’s change (412 if it no longer matches).

Terminal window
curl -X PATCH \
-H "Authorization: Bearer $KUPE_API_KEY" \
-H "Content-Type: application/json" \
"$API/registry" \
-d '{"storageLimit": "20Gi"}'

A raise above your plan’s cap is refused with 400 REGISTRY_STORAGE_LIMIT_ABOVE_PLAN. A lower limit is always accepted.

Terminal window
curl -X POST \
-H "Authorization: Bearer $KUPE_API_KEY" \
-H "Content-Type: application/json" \
"$API/registry/bindings" \
-d '{"cluster": "build", "pushNamespaces": ["ci"]}'
Field Required Mutable Description
cluster Yes No The cluster to connect. It must exist. The connection is named after it.
pushNamespaces No Yes Up to 10 namespaces that get a push Secret, each listed once, none kube-* or argocd. The registry’s connections list at most 20 between them.
secretName No No The push Secret’s name in each push namespace. Default kupe-registry-push; it cannot be kupe-registry-pull.

Every pod of the cluster can then pull from the registry. A push Secret can pull from and push to every repository in your registry, including replacing existing tags. Credentials are never returned by the API.

GET $API/registry/bindings/build shows the connection’s state:

{
"name": "build",
"registry": "registry",
"cluster": "build",
"pushNamespaces": ["ci"],
"secretName": "kupe-registry-push",
"status": {
"phase": "Ready",
"message": "connected: new pods in cluster build can pull from registry.kupe.cloud/acme; restart workloads started earlier to use it",
"pullSecretName": "kupe-registry-pull",
"pullUsername": "kupe+acme+pull-build",
"pushUsername": "kupe+acme+push-build",
"namespaces": [{ "name": "ci", "phase": "Ready", "message": "" }],
"rotation": { "requestedAt": "", "appliedAt": "", "pending": false },
"conditions": []
},
"createdAt": "2026-10-21T10:05:00Z",
"resourceVersion": "582020"
}

status.phase is Ready when pulls work and every push namespace is ready.

PATCH replaces the list: send the whole new list, or [] to remove every push Secret. pushNamespaces is the only field that can change. To add or remove one namespace, GET the connection, edit its list and send it with If-Match set to the ETag you read.

Terminal window
curl -X PATCH \
-H "Authorization: Bearer $KUPE_API_KEY" \
-H "Content-Type: application/json" \
-H "If-Match: $ETAG" \
"$API/registry/bindings/build" \
-d '{"pushNamespaces": ["ci", "release"]}'

A namespace you remove loses its Secret, and the push credential is replaced: build jobs in the remaining push namespaces must restart to read the new Secret.

Terminal window
curl -X POST \
-H "Authorization: Bearer $KUPE_API_KEY" \
"$API/registry/bindings/build/rotate"

202 Accepted returns requestedAt. The rotation is applied when status.rotation.appliedAt on the connection equals it, and status.rotation.pending is false. Jobs that read a push Secret once at start must restart after that.

DELETE $API/registry/bindings/build disconnects the cluster.

Terminal window
curl -X POST \
-H "Authorization: Bearer $KUPE_API_KEY" \
-H "Content-Type: application/json" \
"$API/registry/tokens" \
-d '{"displayName": "github-actions", "access": "push", "expiresInDays": 30}'
Field Required Description
displayName Yes A name to recognise the token by: 1 to 128 printable characters.
access Yes pull, or push, which includes pull.
expiresInDays No 1 to 365, default 90.

The 201 response carries the token’s secret exactly once:

{
"id": "rt-0a1b2c3d4e5f6a7b",
"displayName": "github-actions",
"kind": "token",
"access": "push",
"createdBy": "alice@acme.example",
"createdAt": "2026-10-21T10:00:00Z",
"expiresAt": "2026-11-20T10:00:00Z",
"registry": "registry.kupe.cloud",
"username": "kupe+acme+tok-0a1b2c3d4e5f6a7b",
"secret": "<shown once>",
"status": { "phase": "" }
}

status.phase is empty until the platform has written it, and the token is issued within seconds. Until GET $API/registry/tokens/{id} shows status.phase Ready, the registry refuses it. Failed means it can never work, so create another. Sign in with docker login registry.kupe.cloud --username <username> --password-stdin.

GET $API/registry/tokens lists access tokens and login sessions, newest first, never with a secret. ?kind=token or ?kind=session lists one kind. Each item has lastUsedAt once the token has been used. DELETE on a token revokes it.

POST $API/registry/sessions, with no body, creates a 12-hour login session for the signed-in member, as kupe registry login does: push for an admin, pull for a readonly member. It needs a person’s sign-in, so a request with an API key gets 403 session_requires_user. A member can have 5 live sessions at most, a new one revokes their oldest, and at most 3 logins per 10 seconds are accepted (429 with Retry-After).

Terminal window
curl -H "Authorization: Bearer $KUPE_API_KEY" "$API/registry/repositories?search=team-a"
curl -H "Authorization: Bearer $KUPE_API_KEY" "$API/registry/repositories/team-a%2Fapi/images?tagged=false"

{repository} is the name without the registry host and tenant, with each / sent as %2F: team-a/api is team-a%2Fapi. Lists are paged with page and pageSize, and return items, page, pageSize and total.

An image item carries digest, type (image, chart, index or other), size in bytes, pushedAt, lastPulledAt once it has been pulled, tags, platforms, and pull.byDigest and pull.byTag, the references to pull it by. A multi-platform image is one item of type index listing its platforms. imageCount on a repository counts the index and each of its platform images.

Terminal window
# One tag; the image stays
curl -X DELETE -H "Authorization: Bearer $KUPE_API_KEY" \
"$API/registry/repositories/team-a%2Fapi/tags/old"
# One image, by digest, with every tag on it
curl -X DELETE -H "Authorization: Bearer $KUPE_API_KEY" \
"$API/registry/repositories/team-a%2Fapi/images/sha256:<digest>"
# A repository, with every image and tag in it
curl -X DELETE -H "Authorization: Bearer $KUPE_API_KEY" \
"$API/registry/repositories/team-a%2Fold-api"

Each returns 204. An image is deleted by digest only: a tag in its place is 400. A platform image of a multi-platform image is refused with 409 image_in_use: delete the multi-platform image instead. Deleted images cannot be restored. The stored total falls at once, and the space is freed by the registry’s weekly clean-up.

Deletion protection is on by default. Turn it off, then delete:

Terminal window
curl -X PATCH \
-H "Authorization: Bearer $KUPE_API_KEY" \
-H "Content-Type: application/json" \
"$API/registry" \
-d '{"deletionProtection": false}'
curl -X DELETE -H "Authorization: Bearer $KUPE_API_KEY" "$API/registry"

204 starts the deletion. Every repository, image and tag goes, together with the registry’s cluster connections and access tokens, and none of it can be restored. While protection is on, the DELETE is refused with 409 deletion_protected. A tenant cannot be deleted while it has a registry (409 registries_exist).

Besides the statuses every route can return (see Errors, retries, and rate limits), the registry routes use these codes:

Status code When
400 REGISTRY_STORAGE_LIMIT_ABOVE_PLAN A storage limit above your plan’s cap (field: spec.storageLimit).
400 REGISTRY_PUSH_NAMESPACES_LIMIT More than 10 push namespaces in one connection (field: spec.pushNamespaces).
400 REGISTRY_PUSH_NAMESPACES_SERVICE_LIMIT More than 20 push namespaces across the registry’s connections (field: spec.pushNamespaces).
400 REGISTRY_TOKEN_EXPIRY_INVALID An expiry outside 1 to 365 days (field: spec.expiresAt).
400 plan_limit_reached Your plan includes no registry.
400 creation_paused The platform has paused new registries. Retry later.
403 session_requires_user A login session requested with an API key.
403 REGISTRY_TOKEN_PUSH_ADMIN_ONLY A push token whose creator is no longer an admin.
403 REGISTRY_TOKEN_CREATOR_NOT_MEMBER A token whose creator is no longer a member.
404 registries_unavailable The registry is not available on the platform. Retrying does not help.
409 REGISTRY_ALREADY_EXISTS The tenant already has a registry.
409 REGISTRY_BINDING_EXISTS The cluster is already connected.
409 REGISTRY_TOKEN_LIMIT_REACHED The registry already has 50 access tokens.
409 deletion_protected DELETE while deletionProtection is true.
409 registries_exist Deleting a tenant that has a registry.
409 registry_not_ready A repository, image or tag route before the registry is provisioned.
409 image_in_use Deleting a platform image of a multi-platform image.
409 image_protected The registry refused to delete an image or tag for another reason.
409 terminating The registry, the connection or the cluster is being deleted.
412 The If-Match ETag no longer matches.
429 More than 3 logins in 10 seconds for one member; retry after Retry-After.
503 The registry is busy or briefly unavailable. Retry in a few seconds.

While the registry is switched off on the platform, the clean-up routes keep working so you can always remove what exists: listing and getting the registry, a PATCH that only sets deletionProtection, DELETE, listing, getting and deleting connections, a connection PATCH that only removes push namespaces, every repository, image and tag route, and listing and revoking tokens.