Reference
API reference
REST API overview — authentication, scopes, errors, pagination, caching, idempotency, rate limits and the upload flow.
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 "$B/artifacts/<ns>/<slug>/history"
latest is accepted in place of a version.
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 |
| 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.