title: API reference description: REST API overview — authentication, scopes, errors, pagination, caching, idempotency, rate limits and the upload flow. order: 31 section: Reference
API reference
Base URL: https://skills.spboucher.ai/api/v1
The complete, machine-readable description is the OpenAPI 3.1 document, generated from the server's route table:
Reading needs no account. Responses are JSON unless noted.
Discovery
B=https://skills.spboucher.ai/api/v1
curl -s "$B/artifacts?type=skill&compatibility=claude-code&sort=updated&per_page=10"
curl -s "$B/search?q=citation+check"
curl -s "$B/categories" # types with live counts, licences, keywords, totals
curl -s "$B/collections"
curl -s "$B/artifacts/<ns>/<slug>" # metadata + latest release
curl -s "$B/artifacts/<ns>/<slug>/versions"
curl -s "$B/artifacts/<ns>/<slug>/versions/1.2.0" # manifest, files, dependencies, compatibility, scan
curl -s "$B/artifacts/<ns>/<slug>/versions/1.2.0/files?path=SKILL.md"
curl -s "$B/artifacts/<ns>/<slug>/versions/1.2.0/install?client=claude-code"
curl -s "$B/artifacts/<ns>/<slug>/versions/1.2.0/checksum" # text: "<sha256> <slug>-1.2.0.tar.gz"
curl -fsSLO "$B/artifacts/<ns>/<slug>/versions/1.2.0/download" # application/gzip
curl -s -o shot.png "$B/artifacts/<ns>/<slug>/versions/1.2.0/media/examples/shot.png" # a screenshot listed in the manifest `examples`
curl -s "$B/artifacts/<ns>/<slug>/history"latest is accepted in place of a version.
Every release object carries examples: the screenshots the publisher listed in the manifest, each with the url the registry serves it from (release detail adds width, height, sizeBytes and mime). The /media/ route serves only those listed paths, as image/png, image/jpeg or image/webp, with X-Content-Type-Options: nosniff and Content-Security-Policy: default-src 'none'; any other path of the archive answers 404, and a revoked release answers 410.
Filters on /artifacts: q, type, keyword, compatibility, license, namespace, capability, sort (relevance, updated, created, name, downloads), page, per_page.
Search uses PostgreSQL full-text search with prefix matching, plus trigram matching as a typo fallback for queries of five characters or more. There is no semantic search.
Pagination
List responses have this shape:
{ "items": [], "page": 1, "perPage": 24, "total": 0, "totalPages": 1 }page starts at 1. per_page is 1–100, default 24. Ordering is stable: every sort ends with a unique tie-breaker.
Authentication
Authorization: Bearer skr_<redacted>Tokens are created in the dashboard, with the CLI, or by the operator. A token carries scopes, a list of namespaces, an expiry and optionally an IP allowlist. It is shown once and cannot be retrieved later.
Never put a token in a URL. Authenticated responses are sent with Cache-Control: private, no-store.
The dashboard uses a session cookie instead; cookie-authenticated writes need a same-origin request and a CSRF token, so scripts should use bearer tokens.
Scopes
| Scope | Allows |
|---|---|
artifacts:read |
Read artifacts in allowed namespaces, including non-public ones |
artifacts:create |
Create artifacts in allowed namespaces |
artifacts:update |
Update summary, description, links, keywords, visibility |
artifacts:deprecate |
Deprecate, undeprecate, archive |
uploads:create |
Begin uploads and send archive bytes |
releases:publish |
Finalize an upload into an immutable release |
releases:update |
Edit release notes only |
releases:revoke |
Revoke a release |
tokens:read |
List your tokens (metadata only) |
tokens:write |
Create or revoke tokens, never broader than the caller |
audit:read |
Read your audit trail |
admin:moderate, admin:settings |
Elevated; platform administrators only, tokens limited to 24 hours |
A write also requires that you own the namespace and, for a token, that the namespace is on the token.
Errors
{
"error": {
"code": "insufficient_scope",
"message": "This token lacks the \"artifacts:create\" scope.",
"details": { "required": "artifacts:create" },
"requestId": "0b9c…"
}
}Every response carries X-Request-Id. Quote it when reporting a problem.
| Status | Typical codes |
|---|---|
| 400 | bad_request |
| 401 | unauthorized, invalid_token, token_expired, token_revoked, invalid_credentials |
| 403 | insufficient_scope, namespace_forbidden, reauth_required, admin_required, csrf |
| 404 | not_found (also returned for private artifacts you cannot see) |
| 405 | method_not_allowed |
| 409 | version_exists, artifact_exists, release_immutable, upload_state, conflict |
| 410 | release_revoked, taken_down, upload_expired |
| 413 | payload_too_large |
| 415 | unsupported_media_type, not_previewable |
| 422 | release_rejected (with details.findings), checksum_mismatch, idempotency_key_reused |
| 428 | confirmation_required |
| 429 | rate_limited (with Retry-After) |
| 500 | internal_error (no internals are disclosed; use the request id) |
Caching and ETags
- Anonymous JSON reads:
Cache-Control: public, max-age=30and a weakETag. SendIf-None-Matchto get304. - Downloads of public releases:
Cache-Control: public, max-age=31536000, immutable,ETag: "sha256-<hex>",X-Checksum-Sha256,Repr-Digest. - Cross-origin
GET,HEADandOPTIONSare allowed from any origin. Writes are not available cross-origin from browsers.
Rate limits
Limits are per token, else per signed-in user, else per IP address. Responses include RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.
| Kind of request | Limit |
|---|---|
| Anonymous reads | 240 per minute |
Example images (/media/) |
600 per minute |
| Search | 90 per minute |
| Downloads | 120 per minute |
| Authenticated calls | 120 per minute |
| Upload begin / upload bytes | 30 per hour |
| Finalize (publish) | 30 per hour |
| Abuse reports | 5 per hour |
| Sign-in attempts | 8 per 15 minutes |
Twenty failed bearer authentications from one address within ten minutes block further bearer authentication from that address until the window ends.
Idempotency
POST /artifacts, POST /artifacts/{id}/releases and POST /uploads/{id}/finalize accept an Idempotency-Key header (8–128 characters: letters, digits, _ . : -).
- Repeating a request with the same key and body returns the stored response with
Idempotent-Replayed: true. - Reusing a key with a different body returns
422 idempotency_key_reused. - Finalize is also naturally idempotent: finalizing an already published upload returns the same release with status 200.
Publishing flow
Three steps after the artifact exists. The archive is a deterministic .tar.gz (the CLI's skills pack produces it).
B=https://skills.spboucher.ai/api/v1
AUTH="Authorization: Bearer $SKILLS_TOKEN" # set in this shell only
F=citation-check-1.0.0.tar.gz
SHA=$(shasum -a 256 "$F" | cut -d' ' -f1); SIZE=$(wc -c < "$F" | tr -d ' ')
# 0. create the artifact (once). Private by default; public needs confirmPublic.
curl -s -X POST "$B/artifacts" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"namespace":"yourns","slug":"citation-check","type":"skill","summary":"Checks that every citation resolves to a reference.","visibility":"public","confirmPublic":true}'
ID=$(curl -s "$B/artifacts/yourns/citation-check" -H "$AUTH" | jq -r .id)
# 1. begin: declare version, checksum and size
UP=$(curl -s -X POST "$B/artifacts/$ID/releases" -H "$AUTH" -H 'Content-Type: application/json' \
-H "Idempotency-Key: begin-$SHA" \
-d "{\"version\":\"1.0.0\",\"sha256\":\"$SHA\",\"sizeBytes\":$SIZE,\"releaseNotes\":\"Initial release.\"}" | jq -r .uploadId)
# 2. send the bytes
curl -s -X PUT "$B/uploads/$UP" -H "$AUTH" -H 'Content-Type: application/gzip' --data-binary "@$F"
# 3. finalize: validate, scan, publish
curl -s -X POST "$B/uploads/$UP/finalize" -H "$AUTH" -H 'Content-Type: application/json' -d "{\"sha256\":\"$SHA\"}"- An upload session expires after 60 minutes and belongs to the user who began it.
- The bytes must match the declared size and SHA-256 exactly, or nothing is stored (
422 checksum_mismatch). - Finalize returns
201withrelease.stateequal topublishedorquarantined, plus the list of findings. A rejected release returns422 release_rejectedwith the blocking findings; fix and upload again under the same version. GET /uploads/{id}returns the session state.
Changing things afterwards
| Request | Effect |
|---|---|
PATCH /artifacts/{id} |
summary, description, repository, homepage, issues, keywords, visibility (+ confirmPublic when exposing). Namespace, slug and type cannot change |
PATCH /releases/{id} |
releaseNotes only. Anything else returns 409 release_immutable |
POST /artifacts/{id}/deprecate |
{ "reason": "…", "confirm": true } |
POST /artifacts/{id}/undeprecate |
— |
POST /artifacts/{id}/archive |
{ "confirm": true } |
POST /releases/{id}/revoke |
{ "reason": "…", "confirm": true } — permanent |
Account
| Request | Notes |
|---|---|
GET /me |
Identity, credential scopes and namespaces |
GET /me/tokens |
Metadata only |
POST /me/tokens |
{ name, scopes, namespaces?, expiresInHours?, ipAllowlist? } → the token, once |
DELETE /me/tokens/{id} |
Revoke |
POST /me/tokens/{id}/rotate |
Replacement token, once; the old one is revoked |
GET /me/audit |
Your actions: before (cursor), limit, action (prefix) |
POST /validate-manifest |
Checks a manifest object against the schema without uploading |
Reporting
curl -s -X POST "$B/reports" -H 'Content-Type: application/json' \
-d '{"artifact":"@ns/slug","version":"1.2.0","category":"malware","details":"At least twenty characters describing the problem.","contact":"optional@example.org"}'Categories: malware, secret-leak, license, impersonation, spam, broken, other. No account needed.
Health
GET /health/live and GET /health/ready (database and storage).
SDK
A typed TypeScript client lives in the repository (packages/sdk, class RegistryClient). It is not yet published to npm.