Creating a repository
Every route and code below is in the reference.
POST /artifacts/repos{ "displayName": "docs", "description": "Our handbook", "remoteAccess": true }The caller owns it. The answer is 201, with the repository as they see it: its physical name, its displayName, its owner, their access and remoteAccess.
The physical name is a random UUID. It is the one in the clone URL, and nobody chose it, so one tenant’s docs cannot collide with another’s and nobody can probe for a name. The display name is unique per owner and nowhere else. A second docs for the same owner is artifacts/name_taken.
Cloudflare answers a create with a token: write scope, a day’s life. That is a long-lived credential this capability never issues, so the route revokes it before it answers. A revoke that fails deletes the repository again, so nothing is left that no row can reach.
Your own naming convention is a seam. Pass repoName to artifacts(), or set acceptRepoName and send name in the body. Then uniqueness is yours. The name is held to Cloudflare’s charset and the 63 cap, and refused rather than shortened.
Organization repositories
With organization composed, a repository may belong to an organization.
POST /artifacts/organization/repos
GET /artifacts/organization/reposBoth need an organization in force and a role holding your managePower. A member with that power is the owner of every repository the organization holds. Without organization composed, these routes do not exist.
Giving somebody access
POST /artifacts/repos/:name/grants{ "userId": "usr_…", "grant": "write" }A grant is read or write, and nothing else. That is Artifacts’ own token scope: read is clone, fetch and pull, write is all of that plus push. No role vocabulary maps onto two values by itself, so the kit does not guess. You decide what your roles mean, and write it as grants.
Only the owner may set or remove one. Posting again changes it. DELETE /artifacts/repos/:name/grants/:userId takes it away.
A changed or removed grant does not end a token already minted. It lives out its life. The next mint is refused, or minted at the new scope. To end it now, revoke every token.
Minting a token
POST /artifacts/repos/:name/credentials{
"token": "art_v1_…?expires=1792425600",
"scope": "read",
"expiresAt": "2026-10-19T17:00:00.000Z",
"remote": "https://<ACCOUNT_ID>.artifacts.cloudflare.net/git/acme-prod/<name>.git"
}The scope is the caller’s access. An owner and a write grant get write. A read grant gets read, and can never obtain anything more. The token lives tokenTtlSeconds, fifteen minutes by default.
expiresAt says when to come back. The answer carries Cache-Control: no-store, and the kit keeps no copy.
A git client mints per operation. That is the credential helper, and it is the way to hand this to a person. It uses your app’s own credential, so the only long-lived credential is the one you already revoke in auth.
Ending access now
The owner can revoke every live token on a repository at once.
DELETE /artifacts/repos/:name/credentialsIt answers { "revoked": 3 }. Remove the grant first, then call it. Every helper mints again on its next operation, and the person whose grant is gone is refused. It revokes every token the kit recorded by its id first, so it never depends on Cloudflare’s one-page listing, and then revokes from the listing until it shows none live, which catches a token nobody recorded. A live suite checks that with git against a real namespace before every release.
From a management client
A management client sees every repository, not only the ones it owns, and can take one person’s key back without touching anybody else’s. Its routes sit under /artifacts/admin/, so they never collide with the owner’s.
| Route | Scope | Answers |
|---|---|---|
GET /artifacts/admin/repos | artifacts:repos:read | Every repository, newest first, with its owner, remote access and last push. Narrowed by remoteAccess, ownerKind and ownerId, and paged by cursor and limit |
GET /artifacts/admin/repos/:name | artifacts:repos:read | One repository, its grants, and its commit log, paged by limit and offset |
GET /artifacts/admin/repos/:name/tokens | artifacts:repos:read | Cloudflare’s one page of live tokens: 30, newest first, with the total beside it. Each with its scope, created and expiry, and its holder where the kit recorded one |
POST /artifacts/admin/repos/:name/tokens/:tokenId/revoke | artifacts:tokens:revoke | Revoke one token |
POST /artifacts/admin/repos/:name/tokens/revoke | artifacts:tokens:revoke | Revoke every live token |
The listing narrows on the server. remoteAccess=false is the repositories no git client can reach. ownerKind=org or user is one kind of owner, and ownerId one owner, sent with its ownerKind: an id alone could name an organization or a person who happens to carry it, so it is refused.
Each listed token says who minted it. minted: "pithy" is one the kit minted, and carries its holderId. "outside" is one the kit has no record of, made from Cloudflare’s dashboard or a script, and is never given a holder. "unknown" is one the kit may have minted without a record, created before the kit began recording tokens. The listing holds live tokens only, so every token on it reads state: "active", and a revoked or expired one is not there. No route ever returns a token’s value.
A revoke takes a client-chosen idempotencyKey and a required reason. The first request under a key revokes, and records one artifacts/token_revoked or artifacts/tokens_revoked event with the reason. A retry under the same key answers what the first did, with replayed: true, and revokes nothing, not even a token minted since. The same key sent for a different revocation is a 409. A token the kit minted that is already revoked or expired answers revoked: 0. Any other token that is not live is a 404, like one the repository never held, and spends no key. Revoking every token reaches past the one page the listing shows: it revokes each unexpired token the kit recorded by its id, then from the listing until it shows none live.
Creating, forking or deleting a repository stays the owner’s. No management scope reaches them.
Reading in the Worker
An app that versions its own content reads it without git.
| Route | Answers |
|---|---|
GET /artifacts/repos/:name/log | Commits, newest first, from ref (default HEAD), with limit and offset |
GET /artifacts/repos/:name/commits/:hash | One commit |
GET /artifacts/repos/:name/tree/:hash | One tree’s immediate children |
GET /artifacts/repos/:name/blob/:hash | One blob’s bytes |
GET /artifacts/repos/:name/file?ref=main&path=README.md | One file at a ref |
The bytes are whatever somebody pushed, served from your origin. So they come back with nosniff, Content-Security-Policy: default-src 'none'; sandbox and Content-Disposition: attachment. A browser saves them. It does not run them.
Repositories nobody clones
Create one with "remoteAccess": false. The mint route answers 404, and no response carries the clone URL. Every in-Worker read still answers. That is how an app versions its own content in Artifacts without ever exposing git.
Who can see what
GET /artifacts/repos lists what the caller owns and what they hold a grant on. It is never a listing of the namespace. GET /artifacts/repos/:name adds what Cloudflare holds: the default branch, the last push, and the remote where remote access is on.
A repository you neither own nor hold a grant on answers 404, byte-identical to one that does not exist. The physical name is in the clone URL, and a different refusal would confirm it. Once you can see a repository, a thing you may not do to it is a 403.
What is recorded
With audit composed, every create, delete, grant change, mint and revoke is recorded, against the organization when one owns the repository. A mint records the token’s id, scope and expiry. A management client’s revoke is recorded as its own actor, with its reason. No row ever carries the token, and neither does any log line or error body.
The kit also keeps its own record of every token it mints, in pithy_artifacts_tokens: the token’s id, the repository, the person it was minted for, its scope and its expiry. That is what names a holder in the management view. When a person’s account is purged, their records stay and lose their id to an erased- stand-in, so a token still listed reads as the kit’s, never as one minted outside Pithy.
The error codes worth handling
| Code | Status | Means |
|---|---|---|
artifacts/not_found | 404 | No such repository, or one you may not see. Also the mint route with remote access off |
artifacts/forbidden | 403 | One you can see and may not do this to |
artifacts/name_taken | 409 | That display name is taken for this owner |
artifacts/invalid_name | 400 | A supplied physical name breaks the rule |
Artifacts is Workers Paid only. On a Free plan the first create is refused at runtime. Plan, prices and limits has the rest.