Skip to content
GitmoapdocsBeta
Guide· How do I…?

Package registries

Overview

Publish private npm, Cargo and PyPI packages to a group and install them with the tools you already have. npm publish, cargo publish and twine upload all work against a Gitmoap registry, and npm install, cargo build and pip install read from it — authenticated with the same access token as the rest of the platform, authorized by the same group membership, and charged against the same storage budget as the group's repositories.

A registry belongs to a group, not to a repository, which is what makes it usable across an organisation: the people who can see a group's code are the people who can install its packages, and a monorepo publishing twelve packages needs no extra plumbing. There is nothing to create — a registry starts existing the first time somebody publishes to it.

Quick start

Quick start2 steps

Publish an npm package

You’ll need an access token that can publish packages, exported as GITMOAP_TOKEN.
  1. Point your package's scope at this group's registry in the project's .npmrc, with your own scope in place of @your-scope:

    @your-scope:registry=https://registry.example.com/packages/0192f4c1-7a3e-7c2d-9b1e-5f6a7b8c9d0e/npm/
    //registry.example.com/packages/0192f4c1-7a3e-7c2d-9b1e-5f6a7b8c9d0e/npm/:_authToken=${GITMOAP_TOKEN}
  2. Publish from the package's directory. It appears under Packages with its newest version.

    npm publish
Quick start2 steps

Publish a crate

You’ll need an access token that can publish packages, exported as GITMOAP_TOKEN.
  1. Declare the registry in .cargo/config.toml — note the sparse+ prefix:

    [registries.gitmoap]
    index = "sparse+https://registry.example.com/packages/0192f4c1-7a3e-7c2d-9b1e-5f6a7b8c9d0e/cargo/"
  2. Publish with the token in the environment. The crate appears under Packages.

    CARGO_REGISTRIES_GITMOAP_TOKEN="$GITMOAP_TOKEN" cargo publish --registry gitmoap
Quick start2 steps

Publish a Python package

You’ll need an access token that can publish packages, exported as GITMOAP_TOKEN, and build and twine installed.
  1. Build the distributions from the project's directory:

    python -m build
  2. Upload them with the token as the password. The project appears under Packages.

    TWINE_USERNAME=__token__ TWINE_PASSWORD="$GITMOAP_TOKEN" twine upload --repository-url https://registry.example.com/packages/0192f4c1-7a3e-7c2d-9b1e-5f6a7b8c9d0e/pypi/ dist/*

Before you start

  • A group, and its id.Every registry URL is addressed by the group's id rather than its path, so renaming or moving the group never breaks a build. Take the id from the group's page in the web app, or from gitmoap groups get-by-path --full-path acme/platform.
  • Access on the group.Reporter to install and to read package metadata, Developer to publish and to yank or restore a Cargo version, Maintainer to delete a version or a whole package. Access inherits down a group hierarchy, so Developer on a parent group is Developer on its children's registries.
  • A token with the right scope.Package managers authenticate with a Gitmoap access token — a personal one (gmp_…) or a service-account one (gms_…) for CI. The scopes are package:read to install, package:write to publish and yank, package:delete to delete. Scopes are chosen when the token is minted and cannot be added afterwards, so a CI job that only restores dependencies should hold a token carrying package:read and nothing else.
  • Visibility decides who can install without signing in.A public group's packages are readable by anyone, including anonymous clients. An internal group's are readable by anyone signed in. A private group's are readable only by its members, and a signed-in non-member is answered 404 rather than 403 so the group's existence stays private. A client carrying no token at all is answered 401 by anything that is not public.
  • The client tools themselves.npm reading a token from .npmrc, Cargo with the sparse-registry protocol, and twine plus pip for Python. Nothing needs a plugin.

How it works

One registry per group and per format

A group has up to three registries — one for each ecosystem it speaks — and they are independent of each other. These are the URLs you point a tool at, with the group's id substituted in:

FormatWhat you point the tool at
npmhttps://api.gitmoap.com/packages/{group_id}/npm/
Cargosparse+https://api.gitmoap.com/packages/{group_id}/cargo/
PyPI — installinghttps://api.gitmoap.com/packages/{group_id}/pypi/simple/
PyPI — uploadinghttps://api.gitmoap.com/packages/{group_id}/pypi/

Inside a registry, everything hangs off the package name in a fixed shape. The management endpoints address the package and the version; the file is the level a download names:

group
└── registry npm | cargo | pypi
└── package lodash
└── version 4.17.21
└── file lodash-4.17.21.tgz

A published version is immutable

A (package, version, filename) coordinate can be written once. Publishing the identical bytes to a coordinate that already holds them succeeds and changes nothing — which is what makes a retried CI job safe. Publishing different bytes to a coordinate that is taken is refused with 409 CONFLICT, and nothing about the stored version moves.

Where the tool declares a digest, it is checked against the bytes received before anything is stored: npm's SHA-512 integrity string and its SHA-1 shasum, and the sha256_digest twine sends with each file. A digest that does not match is 400 BAD_REQUEST, and the artifact is discarded. cargo publish declares nothing, so the SHA-256 recorded in the index is computed here from the bytes that arrived — and that computed checksum is what Cargo verifies every later download against.

Yank hides a version, delete destroys it

The two are not variations on the same idea.

Yank (Cargo only)Delete
What happensa flag flips on the versionmetadata and bytes are removed
The filestays exactly where it isgone
Storageunchangedreleased back to the namespace budget
Existing lockfileskeep resolvingfail to resolve
New dependency resolutionskips the versioncannot see the version
Reversibleyes, with unyankno
Access neededDeveloperMaintainer

Yank is the right answer for a bad release: it stops the version being picked up by anything new without breaking anyone who already depends on it. Delete is for bytes that must genuinely stop existing.

Names are normalised per ecosystem

Each format has its own idea of what two names being "the same" means, and Gitmoap stores each name the way that format expects. This is the name the management endpoints take, so it is worth knowing:

FormatStored asMy_Package.Core becomes
npmexactly as published, scope keptMy_Package.Core
Cargolowercasedmy_package.core
PyPIlowercased, with runs of ., - and _ collapsed to one -my-package-core

A name may be 1–214 characters of A–Z a–z 0–9 . _ - @ /, may not contain .., and may not start with - or / or end with /. A version is 1–128 characters of A–Z a–z 0–9 . _ + ! -, which covers both semver and PEP 440. A distribution filename is 1–255 characters of A–Z a–z 0–9 . _ + -, so it can carry no path separator and no ... Neither a version nor a filename may start with -.

Downloads are redirects

Fetching a tarball, a .crate or a wheel answers 302 with a URL to the bytes, so the file never travels through the API itself. Every package manager follows that automatically; a hand-written HTTP client has to be told to (curl -L). The URL it redirects to is good for five minutes, so it is not something to cache in a config file or paste into a ticket.

Your token rides whichever header the tool sends

Each ecosystem's client authenticates differently, and all three carriers are accepted. You never convert a token into anything — the same gmp_/gms_ string goes in all three places.

ToolHeader it sendsWhere you put the token
npmAuthorization: Bearer …the _authToken line in .npmrc
CargoAuthorization: <token>Cargo's credentials file, or CARGO_REGISTRIES_<NAME>_TOKEN
pip and twineAuthorization: Basic …the password half; any username works, and __token__ is the convention

A registry serves only what you publish to it

It holds your packages and nothing else — it does not mirror or proxy npmjs.com, crates.io or PyPI. Keep the public registry configured alongside it and route only your own names to Gitmoap: an npm scope mapping, a named Cargo registry, an extra index for pip. Replacing the public default outright means every third-party dependency stops resolving.

Every write is on the record

Publishing, yanking, unyanking and deleting are recorded in the owning group's audit log, with the person or service account that did it. Reads are not — installing a package leaves no audit entry, so the log answers "who shipped this version" and "who removed it", never "who consumed it".

How to

Publish an npm package

Map your scope to the registry and put the token beside it. In the project's .npmrc:

ini
@acme:registry=https://api.gitmoap.com/packages/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/npm/
//api.gitmoap.com/packages/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/npm/:_authToken=gmp_your_token_here

Then npm publish from the package directory. The dist-tag is set at publish time — npm publish --tag beta files the version under beta instead of latest. The tag map a package advertises is exactly the one its most recent publish sent, so a publish under --tag beta leaves beta as the only tag the package carries until the next ordinary publish puts latest back. Standalone npm dist-tag and npm unpublish are not part of the surface: tags are set by publishing, and removal goes through the management endpoints below.

Publishing is something your package manager does, not something the web app has a button for. Open the group, choose Packages, and the package appears in the list with its newest version, how many versions it holds and how much it stores — that is how you confirm a publish landed.

Terminalbash
gitmoap package-registries publish-npm --group-id <id> --package <package>
curlbash
curl -X PUT https://api.gitmoap.example/packages/{group_id}/npm/{package} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Install npm packages from your registry

A consumer needs the same two .npmrc lines. A machine that only installs should use a service-account token minted with package:read alone, so a leaked CI secret cannot publish.

Resolution then works as usual: npm asks for the package document, picks a version, and follows the tarball URL in it. That document carries the tag map, every published version's manifest and each version's publish time. The tarball URLs Gitmoap hands back always point at Gitmoap, whatever was in the tarball's own metadata when it was published.

Open the group, choose Packages, and select the package. The Versions tab lists every published version with its size, its publish time and the person or service account that published it.

Terminalbash
gitmoap package-registries get-npm-packument \
--group-id <id> \
--package <package>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/npm/{package} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

To pull one specific tarball without npm — a checksum audit, a mirror script — request it directly and follow the redirect:

Open the package, find the version in the Versions list, and use the download action at the end of its row. Your browser follows the redirect and saves the tarball.

Terminalbash
gitmoap package-registries get-npm-tarball \
--group-id <id> \
--package <package> \
--version <version> \
--filename <filename>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/npm/{package}/-/{version}/{filename} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Note

A scoped name is one path segment, so its / is percent-encoded as %2F — @acme/ui is @acme%2Fui in a URL you build by hand. npm does this itself; a curl command written from memory usually does not, which is the most common cause of a surprising 404 here.

Search the npm registry

npm search --registry=<your registry URL> <text> works against a Gitmoap registry. Matching is by word and by substring, so serde_j finds serde_json — package names are identifiers, not prose, and a word-stemming search would miss that. Results page by offset, up to 100 at a time.

npm's wire format carries a score block, and the values in it are fixed placeholders rather than measurements — quality, popularity and maintenance are npmjs.com heuristics over download counts and public-repository signals a private registry has no source for. They say nothing about your packages, so read the names and versions and ignore the numbers beside them.

Searching is a package-manager call. To find something in the web app instead, open the group, choose Packages and switch to the npm format tab — the listing is the whole registry, newest change first.

Terminalbash
gitmoap package-registries search-npm --group-id <id> --text <text>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/npm/-/v1/search \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Publish a crate

Declare the registry in .cargo/config.toml, note the sparse+ prefix:

toml
[registries.gitmoap]
index = "sparse+https://api.gitmoap.com/packages/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/cargo/"

Supply the token as CARGO_REGISTRIES_GITMOAP_TOKEN, or store it with cargo login --registry gitmoap. Then cargo publish --registry gitmoap.

The crate name is lowercased when it is stored, so My_Crate and my_crate are one crate — and publishing the same version under both spellings conflicts, because the second .crate file will not be byte-identical to the first.

Cargo publishes; the web app reports. Open the group, choose Packages, and switch to the Cargo format tab to see the new version and how much the crate now stores.

Terminalbash
gitmoap package-registries publish-cargo --group-id <id>
curlbash
curl -X PUT https://api.gitmoap.example/packages/{group_id}/cargo/api/v1/crates/new \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Depend on a crate from your registry

With the registry declared as above, a dependency names it:

toml
[dependencies]
acme-core = { version = "1.2", registry = "gitmoap" }

Cargo reads the registry's configuration document first to learn where to download crate files from, where to send publishes and yanks, and whether it needs to send credentials at all. For a private or internal group that document says authentication is required and Cargo sends your token with index and download requests as well as with publish; for a public group it says none is required, and anyone can depend on the crate with no token configured.

Open the group and choose Packages, then the Cargo tab. The panel shows the registry URL to paste into .cargo/config.toml and whether the registry requires authentication.

Terminalbash
gitmoap package-registries get-cargo-config --group-id <id>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/cargo/config.json \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Check what Cargo sees for a crate

The index is one line of JSON per published version, including the dependency list and the yanked flag. Reading it directly is the fastest way to answer "why did resolution pick that version" without a build.

Which URL serves a crate depends on the length of its name, following the sparse-registry layout: a name of three characters or more lives under two prefix segments (se/rd/serde), and a one- or two-character name lives under a single one (2/rs).

Open the group, choose Packages, select the crate, and open the Versions tab. Each row shows the version, whether it is yanked, its size and who published it. The checksum lives in the index a client reads, not in that view.

Terminalbash
gitmoap package-registries get-cargo-index-long \
--group-id <id> \
--seg1 <seg1> \
--seg2 <seg2> \
--seg3 <seg3>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/cargo/{seg1}/{seg2}/{seg3} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

For a crate whose name is one or two characters:

The web app shows short-named crates in the same package list — the split only affects the URL a client fetches, not what you see.

Terminalbash
gitmoap package-registries get-cargo-index-short \
--group-id <id> \
--seg1 <seg1> \
--seg2 <seg2>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/cargo/{seg1}/{seg2} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

To pull the crate file itself, request the download and follow the redirect:

Open the crate, find the version, and use the download action at the end of its row.

Terminalbash
gitmoap package-registries download-cargo-crate \
--group-id <id> \
--crate-name <crate name> \
--version <id>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/cargo/api/v1/crates/{crate_name}/{version}/download \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Yank a crate version

cargo yank --registry gitmoap --version 1.2.3 acme-core marks a version so that no new resolution picks it, while every build that already pinned it keeps working. Nothing is deleted and no storage is freed. Yanking a version that is already yanked changes nothing and still succeeds.

Open the group, choose Packages, select the crate, and open the Versions tab. Use Yank at the end of the version's row; the row stays in the list with a yanked badge.

Terminalbash
gitmoap package-registries yank-cargo \
--group-id <id> \
--crate-name <crate name> \
--version <version>
curlbash
curl -X DELETE https://api.gitmoap.example/packages/{group_id}/cargo/api/v1/crates/{crate_name}/{version}/yank \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Restore a yanked crate version

cargo yank --undo --registry gitmoap --version 1.2.3 acme-core puts the version back into resolution. Because a yank never removed anything, this is a complete restoration.

On the crate's Versions tab, use Unyank on a yanked row. The badge disappears and the version is resolvable again.

Terminalbash
gitmoap package-registries unyank-cargo \
--group-id <id> \
--crate-name <crate name> \
--version <version>
curlbash
curl -X PUT https://api.gitmoap.example/packages/{group_id}/cargo/api/v1/crates/{crate_name}/{version}/unyank \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Search the Cargo registry

cargo search --registry gitmoap <query> searches the group's crates by word and by substring. Cargo pages by 1-based page number rather than by cursor, up to 100 results per page, because that is what the protocol specifies.

As with npm, searching is a Cargo call. Open the group, choose Packages and switch to the Cargo format tab to browse the registry in the web app.

Terminalbash
gitmoap package-registries search-cargo --group-id <id> --q <q>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/cargo/api/v1/crates \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Publish a Python distribution

Add the repository to ~/.pypirc. The token goes in the password; the username is not what authenticates you, and __token__ is the convention:

ini
[distutils]
index-servers = gitmoap
[gitmoap]
repository = https://api.gitmoap.com/packages/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/pypi/
username = __token__
password = gmp_your_token_here

Then twine upload --repository gitmoap dist/*. Twine sends one file per request, so an sdist and a wheel are two separate publishes and either can succeed while the other fails.

The project name is normalised the PEP 503 way, so My_Package.Core, my-package-core and MY.PACKAGE.CORE are all the same project.

Twine uploads; the web app reports. Open the group, choose Packages, switch to the PyPI tab and the project appears with the version you just uploaded and the bytes it now stores.

Terminalbash
gitmoap package-registries upload-pypi --group-id <id>
curlbash
curl -X POST https://api.gitmoap.example/packages/{group_id}/pypi/ \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Install Python packages from your registry

Point pip at the project index and give it the credentials inline. In pip.conf or pip.ini:

ini
[global]
extra-index-url = https://__token__:gmp_your_token_here@api.gitmoap.com/packages/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/pypi/simple/

The project page lists every distribution file for the project with its SHA-256 attached, which is what pip verifies each download against. It is served as HTML or as JSON depending on what your client asks for, so both a modern resolver and an old one work unchanged.

Open the group, choose Packages, switch to the PyPI tab and select the project. Each version shows its total size, when it was uploaded and who uploaded it; the per-file digests are in the index pip reads.

Terminalbash
gitmoap package-registries get-pypi-project \
--group-id <id> \
--project <project>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/pypi/simple/{project}/ \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Warning

extra-index-url means pip considers both indexes and takes the highest version it finds in either. If a public project on PyPI shares a name with one of yours, pip can silently prefer the public one. Give private projects names that nobody else publishes, or use index-url to make Gitmoap the only index a locked-down build is allowed to read.

List every Python project in a registry

The simple index is the group's PyPI project list — what a resolver walks and what a mirror script starts from. It comes back as HTML for an ordinary client and as JSON for one that asks for it, and it lists every project in the group's PyPI registry, alphabetically, in one response: the simple index has no pages, so nothing is left for a second request.

Open the group, choose Packages and switch to the PyPI tab. The list is the same set of projects the index serves.

Terminalbash
gitmoap package-registries list-pypi-projects --group-id <id>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/pypi/simple/ \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Download one distribution file

Request the file and follow the redirect. Each file's URL carries its project, version and filename, so it is stable and reproducible.

Open the project, find the version, and use its download action. Your browser follows the redirect and saves the file.

Terminalbash
gitmoap package-registries download-pypi-file \
--group-id <id> \
--project <project> \
--version <id> \
--filename <id>
curlbash
curl -X GET https://api.gitmoap.example/packages/{group_id}/pypi/files/{project}/{version}/{filename} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

See what a group has published

The management listing spans all three formats and reports, per package, its latest version, how many versions it has, how many bytes it occupies and when it last changed. Packages come back most-recently-changed first. Pass a format to page through that one ecosystem with the cursor; omit it and you get a single overview page — up to 100 packages per format, with no cursor to continue from.

Latest version means the version published most recently, not the highest one. Publishing 1.0.1 as a patch after 2.0.0 is already out makes 1.0.1 the latest version the listing reports.

Open the group and choose Packages. The list shows every package across the three formats with its latest version, version count and total size, and the format tabs narrow it to one ecosystem.

Terminalbash
gitmoap package-registries list-group-packages --group-id <id>
curlbash
curl -X GET https://api.gitmoap.example/groups/{group_id}/packages \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Inspect a package and its versions

The detail view is the one place that shows every version of a package with its size, whether it is yanked, when it was published and by whom — including publishes made by a service account from a pipeline.

Open the group, choose Packages and select the package. The header carries the total size and version count; the Versions tab carries the per-version detail.

Terminalbash
gitmoap package-registries get-group-package \
--group-id <id> \
--format <format> \
--package-name <package name>
curlbash
curl -X GET https://api.gitmoap.example/groups/{group_id}/packages/{format}/{package_name} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Delete one version

Warning

Deleting a version destroys its files and cannot be undone. Any build that pins that exact version stops resolving immediately — a lockfile referring to it fails rather than falling back. For a Cargo release you merely want to withdraw, yank it instead: a yanked version keeps every existing build working. npm and PyPI have no yank, so withdrawing there means either deleting or leaving the version in place. Deleting is the right move only when the bytes themselves must go, for example a package published with a secret baked into it.

Address the package by the name you published: the delete normalises it the way its format does, so My_Package.Core and my-package-core reach the same PyPI project and My_Crate reaches my_crate. npm is the exception, because npm names are stored exactly as they were published — there the case has to match. The bytes are released back to the namespace's storage budget as part of the delete.

Deleting the last version of a package leaves the package itself in the listing with no versions left in it. Delete the package to take it out of the list.

Open the group, choose Packages, select the package and open the Versions tab. Use the delete action at the end of the version's row; the confirmation names the version and the bytes it destroys.

Terminalbash
gitmoap package-registries delete-group-package-version \
--group-id <id> \
--format <format> \
--package-name <package name> \
--version <id>
curlbash
curl -X DELETE https://api.gitmoap.example/groups/{group_id}/packages/{format}/{package_name}/versions/{version} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Delete a package and every version of it

Warning

This destroys every version of the package and every file in every version, in one action. Nothing is recoverable and no dependent build survives it. If you are retiring a package rather than erasing it, yank its versions where the format allows it, or simply stop publishing new ones — an unused package costs only its stored bytes.

Open the group, choose Packages and select the package. Use Delete package in the header. The confirmation names the package and how many versions and bytes go with it.

Terminalbash
gitmoap package-registries delete-group-package \
--group-id <id> \
--format <format> \
--package-name <package name>
curlbash
curl -X DELETE https://api.gitmoap.example/groups/{group_id}/packages/{format}/{package_name} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Reference

The first-party management surface — the endpoints behind the Packages view, and the ones to script against:

4 endpoints

GET/groups/{group_id}/packagespackage:read

List a group's packages

list_group_packagesGET /groups/{group_id}/packagesOpen in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
Query
  • formatstringoptionalFilter by format (npm/cargo/pypi)
  • limitint32optionalPage size (1-100, default 20)
  • cursorstringoptionalOpaque pagination cursor

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404

Receive

  • items[]object6 fields
    • formatcargonpmpypiThe package ecosystem a registry speaks. A group has at most one registry per format.
    • latest_versionobjectoptionalThe newest version, when there is one.
    • namestringThe package's name, as it was published.
    • total_bytesint64The size of every file of every version, in bytes.
    • updated_atstring
    • version_countint64
  • next_tokenobjectoptional
GET/groups/{group_id}/packages/{format}/{package_name}package:read

A package's detail with its versions

get_group_packageGET /groups/{group_id}/packages/{format}/{package_name}Open in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
  • formatcargonpmpypiRegistry format (npm/cargo/pypi)
  • package_namestringPackage name

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404

Receive

No body.

DELETE/groups/{group_id}/packages/{format}/{package_name}package:delete

Delete an entire package

delete_group_packageDELETE /groups/{group_id}/packages/{format}/{package_name}Open in the API reference

Token scope

package:delete

Send

Path
  • group_iduuidOwning group id
  • formatcargonpmpypiRegistry format
  • package_namestringPackage name

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403
  • 404
  • 409The request conflicts with the resource's current state; `reason` says how

Receive

No body.

DELETE/groups/{group_id}/packages/{format}/{package_name}/versions/{version}package:delete

Delete a single package version

delete_group_package_versionDELETE /groups/{group_id}/packages/{format}/{package_name}/versions/{version}Open in the API reference

Token scope

package:delete

Send

Path
  • group_iduuidOwning group id
  • formatcargonpmpypiRegistry format
  • package_namestringPackage name
  • versionstringVersion

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403
  • 404
  • 409The request conflicts with the resource's current state; `reason` says how

Receive

No body.

The npm protocol surface. These are the shapes npm itself calls; you rarely invoke them by hand:

4 endpoints

GET/packages/{group_id}/npm/{package}package:read

Serve an npm packument

get_npm_packumentGET /packages/{group_id}/npm/{package}Open in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
  • packagestringPackage name (scoped names percent-encoded)

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404
  • 501

Receive

No body.

PUT/packages/{group_id}/npm/{package}package:write

Publish a package version (`npm publish`)

publish_npmPUT /packages/{group_id}/npm/{package}Open in the API reference

Token scope

package:write

Counts against

package_bytes

Send

Path
  • group_iduuidOwning group id
  • packagestringPackage name (percent-encoded)

Refuses

  • 400The document is malformed, or names a different package than the address
  • 401
  • 403
  • 404Not found — or found and private, which is masked as absent
  • 409
  • 429
  • 501

Receive

No body.

GET/packages/{group_id}/npm/{package}/-/{version}/{filename}package:read

Download a package tarball

get_npm_tarballGET /packages/{group_id}/npm/{package}/-/{version}/{filename}Open in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
  • packagestringPackage name (percent-encoded)
  • versionstringVersion
  • filenamestringTarball filename

Refuses

  • 302Redirect to a short-lived download link
  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404
  • 501

Receive

No body.

GET/packages/{group_id}/npm/-/v1/searchpackage:read

Search a group's npm registry

search_npmGET /packages/{group_id}/npm/-/v1/searchOpen in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
Query
  • textstringSearch text (npm search <text>)
  • sizeint32optionalPage size (npm's default 20, max 100)
  • fromint32optionalOffset — npm pages by offset, not cursor

Refuses

  • 400
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404

Receive

No body.

The Cargo protocol surface, including the sparse index in both of its path layouts:

8 endpoints

GET/packages/{group_id}/cargo/config.jsonpackage:read

The Cargo sparse-registry `config.json`

get_cargo_configGET /packages/{group_id}/cargo/config.jsonOpen in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404
  • 501

Receive

No body.

GET/packages/{group_id}/cargo/{seg1}/{seg2}package:read

Sparse-index file for a crate whose name is one or two characters long

get_cargo_index_shortGET /packages/{group_id}/cargo/{seg1}/{seg2}Open in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
  • seg1stringIndex prefix
  • seg2stringCrate name

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404

Receive

No body.

GET/packages/{group_id}/cargo/{seg1}/{seg2}/{seg3}package:read

Sparse-index file for a crate whose name is three or more characters long

get_cargo_index_longGET /packages/{group_id}/cargo/{seg1}/{seg2}/{seg3}Open in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
  • seg1stringIndex prefix part 1
  • seg2stringIndex prefix part 2
  • seg3stringCrate name

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404

Receive

No body.

PUT/packages/{group_id}/cargo/api/v1/crates/newpackage:write

Publish a crate (`cargo publish`)

publish_cargoPUT /packages/{group_id}/cargo/api/v1/crates/newOpen in the API reference

Token scope

package:write

Counts against

package_bytes

Send

Path
  • group_iduuidOwning group id

Refuses

  • 400
  • 401
  • 403
  • 404Not found — or found and private, which is masked as absent
  • 409
  • 429
  • 501

Receive

No body.

GET/packages/{group_id}/cargo/api/v1/crates/{crate_name}/{version}/downloadpackage:read

Download a `.crate` file

download_cargo_crateGET /packages/{group_id}/cargo/api/v1/crates/{crate_name}/{version}/downloadOpen in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
  • crate_namestringCrate name
  • versionstringVersion

Refuses

  • 302Redirect to a short-lived download link
  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404
  • 501

Receive

No body.

DELETE/packages/{group_id}/cargo/api/v1/crates/{crate_name}/{version}/yankpackage:write

Yank a crate version (`cargo yank`)

yank_cargoDELETE /packages/{group_id}/cargo/api/v1/crates/{crate_name}/{version}/yankOpen in the API reference

Token scope

package:write

Send

Path
  • group_iduuidOwning group id
  • crate_namestringCrate name
  • versionstringVersion

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403
  • 404
  • 409The request conflicts with the resource's current state; `reason` says how

Receive

No body.

PUT/packages/{group_id}/cargo/api/v1/crates/{crate_name}/{version}/unyankpackage:write

Restore a yanked crate version (`cargo yank --undo`)

unyank_cargoPUT /packages/{group_id}/cargo/api/v1/crates/{crate_name}/{version}/unyankOpen in the API reference

Token scope

package:write

Send

Path
  • group_iduuidOwning group id
  • crate_namestringCrate name
  • versionstringVersion

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403
  • 404
  • 409The request conflicts with the resource's current state; `reason` says how

Receive

No body.

GET/packages/{group_id}/cargo/api/v1/cratespackage:read

Search a group's Cargo registry

search_cargoGET /packages/{group_id}/cargo/api/v1/cratesOpen in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
Query
  • qstringSearch text (cargo search <q>)
  • per_pageint32optionalPage size (Cargo's default 10, max 100)
  • pageint32optional1-based page number — Cargo pages by number, not cursor

Refuses

  • 400
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404

Receive

No body.

The PyPI protocol surface — a PEP 503 and PEP 691 index, plus the legacy upload endpoint twine posts to:

4 endpoints

GET/packages/{group_id}/pypi/simple/package:read

List the projects in a group's PyPI registry (simple index)

list_pypi_projectsGET /packages/{group_id}/pypi/simple/Open in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Not found — or found and private, which is masked as absent
  • 501

Receive

No body.

GET/packages/{group_id}/pypi/simple/{project}/package:read

List a PyPI project's files (simple index)

get_pypi_projectGET /packages/{group_id}/pypi/simple/{project}/Open in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
  • projectstringProject name

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404
  • 501

Receive

No body.

POST/packages/{group_id}/pypi/package:write

Upload a distribution file (`twine upload`)

upload_pypiPOST /packages/{group_id}/pypi/Open in the API reference

Token scope

package:write

Counts against

package_bytes

Send

Path
  • group_iduuidOwning group id

Refuses

  • 400
  • 401
  • 403
  • 404Not found — or found and private, which is masked as absent
  • 409
  • 429
  • 501

Receive

No body.

GET/packages/{group_id}/pypi/files/{project}/{version}/{filename}package:read

Download a distribution file

download_pypi_fileGET /packages/{group_id}/pypi/files/{project}/{version}/{filename}Open in the API reference

Token scope

package:read

Send

Path
  • group_iduuidOwning group id
  • projectstringProject name
  • versionstringVersion
  • filenamestringDistribution filename

Refuses

  • 302Redirect to a short-lived download link
  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404
  • 501

Receive

No body.

Limits

Two caps apply to publishing, and both are enforced before any bytes are kept.

LimitFree-tier valueWhat it applies to
Size of a single published file500 MiBeach tarball, .crate, sdist or wheel
Stored bytes in the namespace5 GiBevery stored byte in the root group — packages, repositories and release assets together

A file over the size cap is refused with 400 BAD_REQUEST and a message naming the byte limit. A publish that would push the group's namespace past its storage budget is refused with 429 TOO_MANY_REQUESTS. Storage is reserved before the bytes are accepted, so a refusal leaves nothing behind — no orphaned file, no half-created version, and no reservation to reclaim.

There is a second ceiling beside the size cap, and unlike the size cap it does not move with your plan: the registry paths accept a request body up to 512 MiB, and a body over that is refused 413 before the handler sees it. It bites npm first, because an npm publish carries the tarball base64-encoded in a JSON envelope, which inflates it by about a third — a tarball much beyond 380 MiB exceeds the request ceiling and is refused as too large before the per-file cap is ever consulted. Cargo and twine send the artifact as raw bytes, so on the Free tier they meet the 500 MiB cap first; on a plan whose per-file cap is larger than 512 MiB, the request ceiling is the real limit for every format.

There is no cap on the number of packages or versions a registry holds. Stored bytes are the only ceiling, which is why deleting matters and yanking does not: a yank leaves the bytes in place and frees nothing, while deleting a version or a package releases its bytes back to the budget immediately.

Downloads are charged against outbound-byte budgets rather than the storage budget: one for the caller, over the current hour and the current day, and one for the namespace over the current day. When a budget is exhausted, a download answers 429 TOO_MANY_REQUESTS naming the budget that tripped, and because the charge is taken before the redirect is issued, a refusal means nothing was sent at all.

Page sizes are bounded on every paged listing: the packages listing takes 1–100 (default 20), npm search up to 100 (default 20, paged by offset), and Cargo search up to 100 (default 10, paged by 1-based page number). A page size outside its range is clamped to the range rather than refused. The PyPI simple index is not paged: it lists every project at once.

To raise a cap, an Owner of the group can request a limit increase; the alternative is to delete versions nobody depends on. Both the storage budget and the caps in force are visible on the group's usage view.

Troubleshooting

400 BAD_REQUESTnaming an invalid format
Cause
The format segment of the URL, or the format filter on the packages listing, is something other than npm, cargo or pypi
What to do
Use one of the three exactly, in lowercase
400 BAD_REQUESTmentioning an invalid package name, version or filename
Cause
The value falls outside the accepted charset — a space, a .., a leading -, or a character outside A–Z a–z 0–9 . _ - @ /
What to do
Rename the package or retag the version. A filename may not contain a path separator or ..
400 BAD_REQUEST— sha256 digest mismatch, integrity (sha512) mismatch or shasum (sha1) mismatch
Cause
The bytes received do not hash to what the client declared: a truncated upload, a proxy that rewrote or recompressed the body, or an artifact rebuilt between packing and publishing
What to do
Republish from a clean build in one step. If a corporate proxy sits in front of the request, exclude the registry host from body rewriting
400 BAD_REQUEST— package file exceeds the … byte limit
Cause
A single file is over the per-file cap
What to do
Split the distribution, strip bundled assets that belong in object storage, or ask an Owner to request a limit increase
413on publish, with no message about the size cap
Cause
The whole request body exceeded 512 MiB — usually an npm tarball whose base64 envelope inflated past the ceiling
What to do
Reduce the tarball below roughly 380 MiB, or publish the large payload as a release asset and depend on it separately
400 BAD_REQUEST— the search text must not be empty
Cause
A search was issued with no query, or with only whitespace
What to do
Give the search a term. Use the group's Packages list to browse without one
401 UNAUTHORIZED
Cause
No token reached the request — including an anonymous fetch from a group that is not Public — or the token is expired or revoked. The most common cause is an .npmrc token line whose host or path does not exactly match the registry URL
What to do
Check the token line matches the registry URL character for character, including the trailing /, then mint a fresh token if it is old
403 FORBIDDENon publish or yank
Cause
You can read the group but your level on it is below Developer, which is what publishing, yanking and restoring need
What to do
Ask an Owner or Maintainer of the group to raise your access level
403 FORBIDDENon delete, where publishing works
Cause
Deleting needs Maintainer, one level above publishing
What to do
Ask an Owner to raise your access level, or ask a Maintainer to delete it for you
403 INSUFFICIENT_SCOPE
Cause
The token is valid but was minted without the scope this call needs
What to do
Mint a new token carrying package:write or package:delete. Scopes are fixed at mint time and cannot be added to an existing token
403 FORBIDDEN— the token is not authorized for this resource
Cause
The token was bound to one group or repository when it was minted, and this registry belongs to another
What to do
Use a token bound to this group or to an ancestor of it, or an unbound one. A binding is fixed at mint time, like a scope
404 NOT_FOUNDfor a group you can see in the web app
Cause
The group is private and the token's owner — you, or the service account — is not a member of it
What to do
A private group answers 404 rather than 403 so its existence is not leaked. Add the token's owner to the group as a Reporter or above, or use a token whose owner is already a member
404 NOT_FOUNDfor a package you published minutes ago
Cause
The npm name differs in case from the one that was published, or the group id or format segment in the URL is wrong
What to do
Copy the name out of the group's Packages list. Cargo and PyPI names are normalised for you on both publish and lookup, so only npm is case-sensitive: @Acme/UI and @acme/ui are two different packages
404 NOT_FOUNDon a scoped npm package fetched by hand
Cause
The / in @acme/ui split into two path segments
What to do
Percent-encode it as @acme%2Fui. npm does this itself; a hand-written request does not
404 NOT_FOUNDyanking or deleting a version
Cause
No version with that exact string exists — a v prefix, a normalised PEP 440 form, or a version somebody already deleted
What to do
Read the package detail, copy the version string it lists, and reissue. Yanking an already-yanked version succeeds, so a 404 here is always the version, never its state
409 CONFLICTon publish
Cause
That name, version and filename already exist with different bytes. Republishing the identical bytes is a success, so this means the artifact genuinely changed
What to do
Bump the version. A published version is immutable; nothing about the stored one was altered by the attempt
429 TOO_MANY_REQUESTSon publish
Cause
The group's namespace storage budget is full
What to do
Delete versions nobody depends on — yanking frees nothing — or have an Owner request a limit increase. Nothing was stored, so no cleanup is needed
429 TOO_MANY_REQUESTSon a download
Cause
An outbound-byte budget is exhausted — the caller's for this hour or this day, or the namespace's for this day. The response names which one tripped
What to do
Wait for that window to roll over, or spread the pull across fewer concurrent jobs. Nothing was transferred, so a retry costs nothing extra
501 NOT_IMPLEMENTED— Package registry is not configured on this deployment
Cause
Package storage is unavailable on Gitmoap's side, so there is nowhere to keep the bytes
What to do
Contact support@gitmoap.com. No client change makes this call work
501 NOT_IMPLEMENTED— Package registry base URL is not configured
Cause
Gitmoap cannot build the absolute download URLs a package document has to carry
What to do
Contact support@gitmoap.com
npm install resolves your package from npmjs.com
Cause
The scope is not mapped to the registry, so npm used its default
What to do
Add the @scope:registry= line to .npmrc. A scope mapping is per scope — a package published without one has no way to route
cargo reports that the registry index could not be found
Cause
The index value is missing the sparse+ prefix, so Cargo looked for a git index
What to do
Prefix the URL with sparse+. The whole value is sparse+https://…/cargo/
pip install picks a public project with the same name
Cause
extra-index-url makes pip consider both indexes and take the highest version, and a public project can outrank yours
What to do
Use names nobody else publishes, or make Gitmoap the only index with index-url for builds that must not reach the internet
A hand-written HTTP client gets an empty body and a 302
Cause
The download answered with a redirect to the bytes and the client did not follow it
What to do
Follow redirects — curl -L. Do not store the redirect target: it expires
A build that pinned a version fails after somebody deleted it
Cause
Deleting removes the bytes, so a pinned resolution has nothing left to fetch
What to do
Republish the artifact under a new version and update the lockfile. Next time, yank the version instead of deleting it — a yanked Cargo version keeps every existing build resolving
Storage did not drop after yanking a bad release
Cause
A yank flips a flag; it never removes a file
What to do
Delete the version if you need the bytes back. Yank and delete are separate actions with separate access levels

FAQ

Can a CI job publish without a person's token?

Yes, and it should. Create a service account on the group, give it Developer, and mint it a gms_ token with package:write. Publishes then show the service account as the publisher, and revoking its token does not disturb anybody's personal access.

Do packages count against the same storage as repositories?

Yes. Stored bytes are budgeted per namespace — the root group — and packages, git repositories and release assets all draw on the same 5 GiB on the Free tier.

What happens to a group's packages when the group is deleted?

They go with it. Deleting a group destroys its registries, every package in them and every stored byte, and releases the storage.

Does renaming or moving a group break my builds?

No. Registry URLs carry the group's id, not its path, so a rename or a move leaves every configured client working.

Can I search PyPI projects the way I search npm and Cargo?

No. The PyPI protocol defines an index, not a query, so there is no ranked search for it the way there is for npm and Cargo. Read the group's Packages list with the format filter set to PyPI, or the simple index itself, and match names on your side.

  • Access tokensminting personal and service-account tokens, and choosing the scopes a package client needs
  • Groupscreating the group a registry belongs to, its visibility, who is a member of it, and where the storage budget is reported
  • Service accountsthe non-human identity a pipeline publishes as
  • Pipelinesrunning the build that produces the artifact you publish
  • Billing and planswhat each plan raises, and how to ask for a bigger storage budget
  • Releasesattaching large binaries to a tagged release when they are not a package anyone installs