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:
export KUPE_TENANT="<tenant>"export API="https://api.kupe.cloud/api/v1/tenants/$KUPE_TENANT/registries"Turn on the registry
Section titled “Turn on the registry”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).
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.
Connect a cluster
Section titled “Connect a cluster”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.
Change the push namespaces
Section titled “Change the push namespaces”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.
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.
Rotate a connection’s credentials
Section titled “Rotate a connection’s credentials”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.
Create an access token
Section titled “Create an access token”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.
Login sessions
Section titled “Login sessions”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).
Browse repositories and images
Section titled “Browse repositories and images”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.
Delete tags, images and repositories
Section titled “Delete tags, images and repositories”# One tag; the image stayscurl -X DELETE -H "Authorization: Bearer $KUPE_API_KEY" \ "$API/registry/repositories/team-a%2Fapi/tags/old"
# One image, by digest, with every tag on itcurl -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 itcurl -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.
Delete the registry
Section titled “Delete the registry”Deletion protection is on by default. Turn it off, then delete:
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).
Error codes
Section titled “Error codes”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.
Next steps
Section titled “Next steps”- Registry: the model behind these routes
- Manage the registry with Terraform
- API reference