Chicorée REST API
This API follows the registry's features: whenever a feature is added, changed or removed, the endpoints that expose it and this documentation change with it in the same release. The revision moves every time — compare it with the changelog before relying on a new field, and read the changelog before upgrading.
Current revision:
2026-09-07.4· Changelog · index:GET https://chicor.ee/api/v1
Everything the web app can do with organizations, repositories, tags and images is available as JSON under /api/v1. The same personal access tokens that authenticate docker login authenticate the API, with the same roles and restrictions, so a token that can push an image can read its scan result, and one limited to a repository sees nothing else.
Authentication
Send the credential in the Authorization header:
export TOKEN=chc_pat_…
curl -H "Authorization: Bearer $TOKEN" https://chicor.ee/api/v1/me
# Basic auth works too — the token is the password, the user name is ignored:
curl -u "me:$TOKEN" https://chicor.ee/api/v1/me
| Credential | Where it comes from | What it can do |
|---|---|---|
Personal access token chc_pat_… |
Settings → Access tokens | Acts as its user. A read-only token can only read; a read & write token can also change things. A token limited to an organization or to a repository list sees and changes nothing outside it, and cannot search or create repositories. |
Service account chc_sa_… |
Organization → Service accounts | Reads its organization's repositories (or its repository list) plus public ones. With the admin permission it can delete tags and images there. It cannot manage repositories, star, or read members and the audit log. |
CI credential chc_ci_… |
POST /api/v1/auth/exchange with the workflow's OIDC token |
The same rights as a service account with the trusted identity's permission and repository list, for the lifetime of the job (at most an hour). |
| Browser session | Being signed in | The same rights as in the web app — handy for trying calls in the browser. |
| None | — | Public repositories, tags, images and scan results. |
Expired tokens, banned accounts and unknown secrets answer 401; a valid credential without the right answers 403 with the reason. Every use of a token updates its last used time and address (Settings → Access tokens).
Keyless CI authentication
A CI job does not need a stored secret. An organization trusts the workflow's identity once (Organization → Service accounts → CI identities, or the /orgs/{org}/ci-identities endpoints): the issuer of its OIDC tokens and the subject they carry, exact or with * wildcards, plus a permission and an optional repository list. The job then exchanges the token it gets from its CI system for a registry credential:
# GitHub Actions (permissions: id-token: write); the audience is this registry's URL
OIDC=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://chicor.ee" | jq -r .value)
curl -sS -H "Content-Type: application/json" -d "{"token": "$OIDC"}" https://chicor.ee/api/v1/auth/exchange
# → { "token": "chc_ci_…", "expiresAt": "…", "dockerLogin": { "registry": "chicor.ee", "username": "ci", "password": "chc_ci_…" } }
The exchange verifies the token against the issuer's published keys (only issuers some organization trusts are contacted), checks that the audience is https://chicor.ee or chicor.ee, and matches the subject; GitHub subjects look like repo:owner/repo:ref:refs/heads/main, GitLab's like project_path:group/project:ref_type:branch:ref:main. The credential is a signed token with no stored state: deleting the identity revokes it at once. The .github/actions/login action in the repository does all of this and runs docker login.
Administrators can switch the whole API off (Administration → Auth providers → Access, default from API_ENABLED): every endpoint, the index and the OpenAPI document then answer 403 with code api_disabled. docker login and the jobs API are not affected.
Conventions
- Responses are JSON (
application/json, UTF-8). Timestamps are ISO 8601 in UTC (2026-09-05T08:41:12.000Z), sizes are bytes, digests aresha256:<64 hex>. Absent values arenull, not omitted. - Requests with a body send JSON with
Content-Type: application/json. - Paging. Lists take
page(from 1) andper_page(1–100, default 50) and answer{ "items": [...], "page": 1, "perPage": 50, "total": 123, "pages": 3 }. A page past the end returns the last page. - Booleans in the query string are
true/1/yes(anything else is false). - Repository names of proxy caches can be nested (
bitnami/redis); in a path they are one segment with the slash percent-encoded:/repos/dockerhub/bitnami%2Fredis. Top-level images (chicor.ee/nginx) live in thelibraryorganization. - Renamed or transferred repositories are not redirected by the API; use the new name (
docker pulland the web pages do redirect). - Every response carries
X-Api-Version: 1andX-Api-Revision: 2026-09-07.4, andCache-Control: private, no-store. - Changes made through the API are audited like changes made in the app, with
"via": "api"in the entry's details. - Unknown paths under
/api/v1answer a JSON404; an unsupported method answers405. - Conditional requests. Every successful GET carries a weak
ETag; send it back asIf-None-Matchand an unchanged answer comes back as304without a body (the rate-limit and deprecation headers still apply). - Exports.
GET …/manifests/{digest}/vulnerabilities?format=sarifis the image's scan as SARIF 2.1.0 for GitHub code scanning and security dashboards (accepted risks become suppressions);?format=vexis a CycloneDX 1.5 VEX document in which accepted risks arenot_affectedwith their justification and everything else isin_triage. - Rate limits. Requests are counted per credential (per address without one) in fixed windows set by the administrators (Administration → Rate limits, defaults
RATE_LIMIT_API_AUTHENTICATED=1200/1mandRATE_LIMIT_API_ANONYMOUS=120/1m; instance administrators are exempt). Every answer carriesX-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(epoch seconds); over the limit the API answers429 rate_limitedwithRetry-After. - Deprecations. An endpoint that is going away is announced first: its responses carry a
Deprecationheader (andSunsetonce a date is set) with aLinkto this documentation, the reference marks it, and it stays for at least one more revision. Watch the changelog forRemoved:lines.
Errors
{ "error": "This access token is read-only; delete tags here needs a read & write token.", "code": "forbidden" }
| Code | Status | When |
|---|---|---|
bad_request |
400 | A parameter is malformed (a bad digest, an unknown severity, invalid JSON). |
unauthorized |
401 | No usable credential: missing, unknown, expired or a banned account. |
forbidden |
403 | The credential is valid but may not do this (role, read-only token, restriction, service account). |
not_found |
404 | The organization, repository, tag or image does not exist — or is not visible to the caller. |
conflict |
409 | The registry's state refuses the change: a name is taken, a tag is protected, an index member cannot go alone, a scan is already running. |
unprocessable |
422 | The body is well-formed but a value is not acceptable (name rules, quotas, missing fields). |
rate_limited |
429 | Too many requests in the current window; Retry-After says when to try again. |
api_disabled |
403 | An administrator switched the API off (Administration → Auth providers → Access, or API_ENABLED=false); every endpoint answers this until it is on again. |
internal |
500 | Something failed on the server; the details are in the web app's log. |
Some errors add a details object (the offending field, or queued: false when a scan was not started).
Tools
- API browser. The Browse & try tab above lists every endpoint with its parameters, sends real requests with your browser session or a token you paste, and shows the curl line for the same call.
- OpenAPI.
GET https://chicor.ee/api/v1/openapi.jsonis an OpenAPI 3.1 document for Swagger UI, Postman, Insomnia or client generators (response schemas are inferred from the examples below).
Changelog
This API follows the registry's features: whenever a feature is added, changed or removed, the endpoints that expose it and this documentation change with it in the same release. The revision moves every time — compare it with the changelog before relying on a new field, and read the changelog before upgrading.
2026-09-07.4
- Tags:
sizeBytesandlayerCountof a multi-arch tag are those of its first platform variant the registry holds (previously null), and the newsizePlatformnames that platform (linux/amd64); it is null for single-platform images. The tag list and the untagged list on the repository page show the same figures. - Repositories:
kindis judged from that variant when the newest tag is a multi-arch index, so an image pushed for several platforms isimage(previouslyempty).
2026-09-07.3
- Helm charts:
GET /repos/{org}/{repo}/tags/{tag}/chartcarriesprovenancewhen the chart was pushed with its.provfile (which files it names, whether the archive matches, the PGP key id). Tag and manifest details treat a manifest as a chart by its config media type even when Chart.yaml cannot be read.
2026-09-07.2
- Helm charts: repositories carry
kind(image|chart|empty, from the newest tag) andhelmReference(oci://…) for charts; tags carrychart: { name, version, appVersion }; manifest and tag details carrykind,chart(Chart.yaml) andhelmcommands. NewGET /repos/{org}/{repo}/tags/{tag}/chartreturns Chart.yaml, values.yaml, the README and the file list from the archive. For charts,platform,configandscanare null in tag and manifest details.
2026-09-07.1
- Teams:
GET/POST /orgs/{org}/teams,GET/PATCH/DELETE /orgs/{org}/teams/{team},GET /orgs/{org}/teams/{team}/members,PUT/DELETE /orgs/{org}/teams/{team}/members/{userId}. Teams group members of an organization so repository access can be granted to all of them at once. - Per-repository permissions:
GET /repos/{org}/{repo}/access,PUT/DELETE /repos/{org}/{repo}/access/{user|team}/{id}withpermission: pull | push | admin. The organization role stays the baseline for every repository; a grant raises what one person or one team may do in one repository. Docker tokens, the API's write checks and the pages honour grants. GET /repos/{org}/{repo}/size-history?days=— the compressed size of the newest image pushed each day; the repository page charts it for members.- Notation signatures: referrers of type
application/vnd.cncf.notary.signatureare recognised (format: notationin attestation and signature responses), their JWS envelope verified against the embedded certificate, and counted as verified when the signing certificate or its issuer is in the trust store — trusted signing keys now accept X.509 certificate PEMs.
2026-09-06.9
- Webhook format
customwithpayloadTemplate: a JSON body of your own with {{placeholders}} (repository, organization, tag, digest, reference, registry, actor, timestamp, deliveryId, event.); a value that is exactly "{{event}}" embeds the whole event. Webhook responses carry payloadTemplate(null for other formats).
2026-09-06.8
- Webhook format
none: the delivery is the bare request (method of your choice, headers, authentication) with no body — for deploy hooks that read their parameters from the URL and would misread the payload, such as Coolify's POST /api/v1/deploy.
2026-09-06.7
- Webhooks accept
method: GET: the delivery carries no body (event and delivery id stay in the headers, authentication applies as before), for receivers that act on the request itself — a deploy hook such as Coolify's.
2026-09-06.6
- Scan workers: Administration → Scanning → "Offload scans to workers" (SCAN_WORKERS, SCAN_WORKER_TOKEN) hands Trivy scans to external workers over the internal worker protocol (
POST /api/internal/worker/claim,/heartbeat,/tasks/<id>/result,/tasks/<id>/fail, bearer token; not part of /api/v1). The worker is a separate program (chicoree-scan-worker); the protocol is documented in the README. Without a worker online, scans run inline as before. No /api/v1 change. - TRIVY_SERVER_TOKEN (environment only) authenticates the web container and the bundled
trivyserver profile to a Trivy server started with--token, so one vulnerability database can serve every replica and every scan worker.
2026-09-06.5
- The Attestations tab shows the cosign/oras sign-and-attach commands only to viewers who may push to the repository (owner, admin or member of a non-proxy organization, instance administrators); everyone else sees a plain note. No API change.
2026-09-06.4
- The library organization is virtual in the UI: no list, search result, dashboard entry, notification, audit label or job result shows a
library/prefix, and/<name>opens the top-level repository. Storage and the/orgs/library/…routes are unchanged;pathandreferencefields already omitted the prefix. - Explore opens with an overview — trending repositories (pulls in the last 7 days), organizations busiest first with drill-down, recently updated — and
?view=allis the filterable list. No API change. - Anonymous calls to the header search typeahead (
/api/search) count against the anonymous API rate limit per address (RATE_LIMIT_API_ANONYMOUS).
2026-09-06.3
- Editions: Administration → Branding (INSTANCE_EDITION as default) switches the landing page between self-hosted wording and a hosted service — sign-up as the call to action, the free plan named from the default limits, a link to the account portal's plans. No API change.
- Browsing without an account: Explore, search, organization pages and public repositories open for visitors without a session, in a reduced shell with sign-in and sign-up; pages that need a user still redirect to sign-in. No API change.
2026-09-06.2
- Storage enforcement: the quota-enforce job (Administration → Jobs, POST /api/jobs/quota-enforce) notifies organizations and accounts above their storage limit and, after graceDays, removes the oldest images until the limit is met, protected tags excepted, then runs garbage collection. New notification and organization webhook events quota.exceeded and quota.pruned.
2026-09-06.1
- Changed: the plan card on Settings and Organization → Settings appears only while an account portal is configured; a self-hosted registry with plain limits shows users nothing about them. The label on limits rows is documented accordingly.
2026-09-05.5
- Changed: an organization's own limit governs it alone. When an organization has a storage or repository limit of its own, the owners' account limits are not consulted for it and its usage does not count against their accounts; account limits cover the owner's organizations without such a limit. GET /me/usage and GET /users/{userId}/usage report that pool. Enforced the same way by registryd at push time.
- OpenAPI:
integer | nullbody fields are typed as nullable integers, and enums with null carry a JSON null instead of the string "null".
2026-09-05.4
- Member limit: organizations can be capped at a number of members (Administration → Organizations → Limits, maxMembers); an open invitation holds a seat. Enforced when inviting, accepting an invitation, adding a member and on group-binding logins. GET /orgs/{org}/usage reports members and maxMembers.
- Usage: GET /orgs/{org}/usage and the new GET /me/usage carry the month's traffic (pullBytes, redirectBytes, pushBytes, blobPulls, manifestPulls; ?month=YYYY-MM) and the label administrators gave the limits.
- Administration: GET /users (exact email or search), GET /users/{userId}, GET /users/{userId}/organizations, GET /users/{userId}/usage; GET/PATCH/DELETE /orgs/{org}/limits and /users/{userId}/limits read, change and drop limits rows, including a label shown to the owner and an administrators-only note.
- Default limits: Administration → Limits gives every new account and organization a limits row (DEFAULT_USER_MAX_ORGANIZATIONS, DEFAULT_USER_MAX_PUBLIC_REPOS, DEFAULT_USER_MAX_PRIVATE_REPOS, DEFAULT_USER_MAX_STORAGE_GIB, DEFAULT_ORG_MAX_PUBLIC_REPOS, DEFAULT_ORG_MAX_PRIVATE_REPOS, DEFAULT_ORG_MAX_STORAGE_GIB, DEFAULT_ORG_MAX_MEMBERS as defaults).
- Account portal: Administration → Limits (PORTAL_URL, PORTAL_LABEL as defaults) adds a Manage button to the account and organization settings that opens the portal with a one-time token; the portal verifies it with POST /api/auth/one-time-token/verify.
2026-09-05.3
- Retag: PUT /repos/{org}/{repo}/tags/{tag} points a tag at an image already in the repository.
- Promote: POST …/tags/{tag}/copy and POST …/manifests/{digest}/copy copy an image (with variants and attached artifacts) into another repository, creating it when missing.
- Scan gate: GET …/manifests/{digest}/scan waits for a running scan and judges it against a threshold (wait, fail_on, unrated); POST …/scan accepts the same parameters to queue and wait in one call.
- A composite GitHub Action, .github/actions/scan-gate, fails a job on the gate's verdict.
- Organizations: create, rename and delete; usage against limits; policies (default visibility, pull policy, signature policy, member keys) to read and change.
- Service accounts: list, create (secret returned once), details, delete and rotate.
- Members and invitations: change roles, remove members, list, create and cancel invitations.
- Webhooks: list, create, read, update, delete and test, for organizations and repositories.
- Repository policies: read the effective pull and signature policy, change the overrides.
- Exports: GET …/vulnerabilities?format=sarif (SARIF 2.1.0) and ?format=vex (CycloneDX 1.5 VEX) for security dashboards and GitHub code scanning.
- Conditional requests: GET answers carry a weak ETag and honour If-None-Match with 304.
- The OpenAPI document is validated by npm run lint, and administrators see a notice on the overview when the API revision changed since they last acknowledged it.
- Rate limits: requests are counted per credential (per address anonymously) in windows set under Administration → Rate limits (RATE_LIMIT_API_AUTHENTICATED, RATE_LIMIT_API_ANONYMOUS); over the limit the API answers 429 with the new code rate_limited and Retry-After, and every answer carries X-RateLimit-Limit / -Remaining / -Reset.
- Metrics: chicoree_api_requests_total{endpoint,method,status,credential} on the Prometheus endpoint.
- Deprecation policy: endpoints that are going away carry Deprecation, Sunset and Link headers and are marked in the docs for at least one revision before removal.
- Keyless CI authentication: POST /auth/exchange trades a workflow's OIDC token (GitHub Actions, GitLab, any trusted issuer) for a short-lived chc_ci_ credential that works for the API and docker login; organizations manage the trusted identities under /orgs/{org}/ci-identities and in Organization → Service accounts. A login GitHub Action (.github/actions/login) wraps the exchange.
2026-09-05.2
- Administrators can switch the API off (Administration → Auth providers → Access, default from API_ENABLED); every endpoint then answers 403 with the new error code api_disabled.
2026-09-05.1
- Initial release of the REST API under /api/v1.
- Organizations: list, details, repositories, members and the audit log.
- Repositories: create, read, update and delete; tags; untagged manifests; stars.
- Images: manifest details with config, layers and variants; delete by tag or digest; vulnerabilities; signatures, SBOMs and provenance; queue a scan.
- Search across repositories, tags, digests and organizations.
- Personal access tokens, service accounts and the browser session authenticate; read-only tokens are refused on writes.