Overview
The Tank REST API gives you programmatic access to every capability in the registry: searching skills, publishing new versions, managing stars, triggering security scans, and the full CLI OAuth flow. All endpoints live under a single base URL and follow consistent JSON conventions.
Base URL
https://tankpkg.dev/api/v1
For self-hosted deployments, replace the host with your own domain. The /api/v1 prefix is always required.
Authentication
Authenticated endpoints require a Bearer token in the Authorization header.
Authorization: Bearer tank_xxxxxxxxxxxxxxxxxxxxxxxx
Obtaining a token
- CLI: run
tank login— opens GitHub OAuth in your browser, then stores the issued key in~/.tank/config.jsonautomatically. - Dashboard: go to Settings → Tokens and create a token with the scopes you need.
Token prefix
All Tank API keys begin with tank_. Tokens without this prefix are rejected with 401.
Scopes
| Scope | Grants |
|---|---|
skills:read | Read skill metadata, versions, files, stars |
skills:publish | Publish new skills and new versions |
skills:admin | Administrative actions (moderation, user management) |
Tokens inherit the minimum scope needed. A token with only `skills:read` cannot publish; you will receive a `403` if you try.
Rate Limits
Rate limits are applied per IP for anonymous requests and per token for authenticated requests.
| Tier | Requests / hour |
|---|---|
| Anonymous | 100 |
| Authenticated | 1,000 |
| Pro | 10,000 |
When you exceed your limit, the API returns 429 Too Many Requests. The response includes a Retry-After header indicating how many seconds to wait before retrying.
Error Responses
All errors follow a consistent envelope. The error field contains a machine-readable code and the message field contains a human-readable explanation.
{
"error": "UNAUTHORIZED",
"message": "Bearer token is missing or invalid."
}
Common HTTP status codes
| Status | Error Code | When it occurs |
|---|---|---|
401 | UNAUTHORIZED | Token missing, malformed, or revoked |
403 | FORBIDDEN | Token valid but lacks the required scope |
404 | NOT_FOUND | Skill, version, or resource does not exist |
409 | VERSION_EXISTS | You attempted to publish an already-existing version |
422 | VALIDATION_ERROR | Request body failed Zod schema validation |
429 | RATE_LIMITED | Hourly request limit exceeded |
Validation error shape
When a 422 is returned, the issues array mirrors the Zod validation output so you can map errors back to specific fields.
{
"error": "VALIDATION_ERROR",
"message": "Request body validation failed.",
"issues": [
{
"path": ["manifest", "version"],
"message": "Invalid semver string."
}
]
}
Search
GET /api/v1/search
Full-text search across all public skills. Uses a hybrid strategy: ILIKE for exact prefix matching, pg_trgm trigram similarity for fuzzy matching, and a PostgreSQL tsvector full-text index with weighted ranking. Results are ranked by relevance score, then by download count as a tiebreaker.
Authentication: Not required. Private skills are hidden from anonymous results.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | — | Search query. Matched against skill name, description, and author. |
page | number | 1 | Page number. Minimum 1. |
limit | number | 20 | Results per page. Range 1–50. |
Response
{
"results": [
{
"name": "@vercel/next-skill",
"description": "Teaches agents to scaffold, build, and deploy Next.js apps.",
"visibility": "public",
"latestVersion": "2.3.1",
"auditScore": 9,
"publisher": "vercel",
"downloads": 14820,
"stars": 312,
"updatedAt": "2026-03-01T11:00:00.000Z"
}
],
"page": 1,
"limit": 20,
"total": 142
}
Example
curl "https://tankpkg.dev/api/v1/search?q=seo+audit&limit=5"
Skills
POST /api/v1/skills — Publish a skill
Initiates skill publication. The response includes a pre-signed uploadUrl; you must PUT the tarball to that URL and then call POST /api/v1/skills/confirm to finalize.
Authentication: Required — skills:publish scope.
Request body
{
"manifest": {
"name": "@acme/my-skill",
"version": "1.0.0",
"description": "Does something useful for AI agents.",
"visibility": "public",
"permissions": {
"network": { "outbound": ["*.acme.com"] },
"filesystem": { "read": ["./src/**"] }
},
"repository": "https://github.com/acme/my-skill"
},
"readme": "# My Skill\nInstall with `tank install @acme/my-skill`.",
"files": ["tank.json", "README.md", "index.md"]
}
| Field | Type | Required | Description |
|---|---|---|---|
manifest.name | string | Yes | Scoped or unscoped skill name. Scoped names (@org/name) require org membership. |
manifest.version | string | Yes | Semver string. Must not already exist for this skill. |
manifest.description | string | Yes | Short description shown in search results. |
manifest.visibility | "public" | "private" | Yes | Public skills are visible to everyone. Private skills require auth. |
manifest.permissions | object | Yes | Declared permission budget. Validated against Zod schema. |
manifest.repository | string | No | Source repository URL for provenance display. |
readme | string | No | Markdown content for the skill's registry page. |
files | string[] | No | List of file paths included in the tarball. |
Validation rules
manifest.namemust match/^(@[a-z0-9-]+\/)?[a-z0-9-]+$/manifest.versionmust be a valid semver string- Scoped skill names require the authenticated user to be a member of that organization
- Version conflict: publishing
1.0.0when1.0.0already exists returns409 - Permission escalation: PATCH bumps with new permissions, or MINOR bumps adding dangerous permissions (
network,subprocess), are rejected with422
Response 201 Created
{
"uploadUrl": "https://storage.tankpkg.dev/tarballs/abc123?token=...",
"skillId": "skill_01HXYZ",
"versionId": "ver_01HABC"
}
Example
curl -X POST https://tankpkg.dev/api/v1/skills \
-H "Authorization: Bearer tank_xxx" \
-H "Content-Type: application/json" \
-d '{
"manifest": {
"name": "@acme/my-skill",
"version": "1.0.0",
"description": "Does something useful.",
"visibility": "public",
"permissions": {}
}
}'
GET /api/v1/skills/:name — Get skill metadata
Returns top-level metadata for a skill plus its latest published version.
Authentication: Not required for public skills. Private skills require a token with skills:read scope and membership in the owning organization.
Path parameters
| Parameter | Description |
|---|---|
name | Skill name, URL-encoded. Scoped names use @org%2Fskill or @org/skill (both accepted). |
Response 200 OK
{
"name": "@vercel/next-skill",
"description": "Teaches agents to scaffold, build, and deploy Next.js apps.",
"visibility": "public",
"latestVersion": "2.3.1",
"publisher": {
"name": "vercel"
},
"createdAt": "2025-11-15T08:30:00.000Z",
"updatedAt": "2026-03-01T11:00:00.000Z"
}
Example
curl https://tankpkg.dev/api/v1/skills/%40vercel%2Fnext-skill \
-H "Authorization: Bearer tank_xxx"
GET /api/v1/skills/:name/versions — List all versions
Returns every published version for a skill, ordered newest first.
Authentication: Same access control as GET /api/v1/skills/:name.
Response 200 OK
{
"name": "@vercel/next-skill",
"versions": [
{
"version": "2.3.1",
"publishedAt": "2026-03-01T11:00:00.000Z",
"auditScore": 9,
"verdict": "PASS"
},
{
"version": "2.3.0",
"publishedAt": "2026-02-15T09:22:00.000Z",
"auditScore": 8,
"verdict": "PASS"
}
]
}
Example
curl https://tankpkg.dev/api/v1/skills/%40vercel%2Fnext-skill/versions
GET /api/v1/skills/:name/:version — Get version metadata
Returns full metadata for a specific published version, including its permissions declaration and security scan verdict.
Authentication: Same access control as GET /api/v1/skills/:name.
Path parameters
| Parameter | Description |
|---|---|
name | Skill name. |
version | Exact semver string (e.g. 2.3.1). |
Response 200 OK
{
"name": "@vercel/next-skill",
"version": "2.3.1",
"description": "Teaches agents to scaffold, build, and deploy Next.js apps.",
"visibility": "public",
"permissions": {
"network": { "outbound": ["*.vercel.com", "*.anthropic.com"] },
"filesystem": { "read": ["./src/**"], "write": ["./dist/**"] },
"subprocess": false
},
"repository": "https://github.com/vercel/next-skill",
"auditScore": 9,
"verdict": "PASS",
"integrity": "sha512-abc123...",
"publishedAt": "2026-03-01T11:00:00.000Z"
}
Example
curl https://tankpkg.dev/api/v1/skills/%40vercel%2Fnext-skill/2.3.1
GET /api/v1/skills/:name/:version/files/:path — Get file content
Returns the raw content of a specific file within a skill version's tarball. Useful for previewing SKILL.md, README.md, or any other file without downloading the full archive.
Authentication: Same access control as GET /api/v1/skills/:name.
Path parameters
| Parameter | Description |
|---|---|
name | Skill name. |
version | Exact semver string. |
path | File path within the tarball (e.g. SKILL.md, src/index.ts). |
Response
Returns the raw file bytes with an appropriate Content-Type header (e.g. text/markdown for .md files, text/plain for unknown extensions).
Example
curl https://tankpkg.dev/api/v1/skills/%40vercel%2Fnext-skill/2.3.1/files/SKILL.md
POST /api/v1/skills/confirm — Finalize publication
After uploading the tarball to the uploadUrl returned by POST /api/v1/skills, call this endpoint to finalize the publish. Tank verifies the SHA-512 integrity hash, triggers the security scanner, and marks the version as published.
Authentication: Required — skills:publish scope.
Request body
{
"skillId": "skill_01HXYZ",
"versionId": "ver_01HABC",
"integrity": "sha512-ZnVuZ3MtYXJlLWNvb2wuanNvbg=="
}
| Field | Type | Required | Description |
|---|---|---|---|
skillId | string | Yes | skillId from the publish initiation response. |
versionId | string | Yes | versionId from the publish initiation response. |
integrity | string | Yes | SHA-512 hash of the tarball in the format sha512-<base64>. |
Response 200 OK
{
"name": "@acme/my-skill",
"version": "1.0.0",
"status": "published",
"auditScore": 8,
"verdict": "PASS"
}
The security scan runs synchronously during confirmation. The `verdict` in the response reflects the result of the 6-stage pipeline. A `FAIL` verdict does not block publication but is prominently displayed on the skill page and in `tank audit` output.
Example
curl -X POST https://tankpkg.dev/api/v1/skills/confirm \
-H "Authorization: Bearer tank_xxx" \
-H "Content-Type: application/json" \
-d '{
"skillId": "skill_01HXYZ",
"versionId": "ver_01HABC",
"integrity": "sha512-ZnVuZ3MtYXJlLWNvb2wuanNvbg=="
}'
Stars
POST /api/v1/skills/:name/star — Star a skill
Adds a star to a skill on behalf of the authenticated user. Idempotent — starring an already-starred skill returns 200 without creating a duplicate.
Authentication: Required — skills:read scope.
Response 200 OK
{ "starred": true, "stars": 313 }
Example
curl -X POST https://tankpkg.dev/api/v1/skills/%40vercel%2Fnext-skill/star \
-H "Authorization: Bearer tank_xxx"
DELETE /api/v1/skills/:name/star — Unstar a skill
Removes a star from a skill. Idempotent — unstarring a skill that was not starred returns 200.
Authentication: Required — skills:read scope.
Response 200 OK
{ "starred": false, "stars": 312 }
Example
curl -X DELETE https://tankpkg.dev/api/v1/skills/%40vercel%2Fnext-skill/star \
-H "Authorization: Bearer tank_xxx"
GET /api/v1/skills/:name/star — Check star status
Returns whether the authenticated user has starred a skill.
Authentication: Required — skills:read scope.
Response 200 OK
{ "starred": true, "stars": 312 }
Example
curl https://tankpkg.dev/api/v1/skills/%40vercel%2Fnext-skill/star \
-H "Authorization: Bearer tank_xxx"
Security Scanning
POST /api/v1/scan — Scan a tarball
Upload a .tar.gz tarball to run Tank's 6-stage security pipeline without publishing. Useful for CI/CD validation or auditing third-party skills before installing them.
Authentication: Not required. Unauthenticated scans are rate-limited more aggressively.
Request
Send the tarball as a multipart/form-data upload with the field name file.
curl -X POST https://tankpkg.dev/api/v1/scan \
-H "Authorization: Bearer tank_xxx" \
-F "[email protected]"
Response 200 OK
{
"verdict": "PASS_WITH_NOTES",
"auditScore": 7,
"summary": {
"critical": 0,
"high": 0,
"medium": 1,
"low": 2
},
"findings": [
{
"stage": "static",
"severity": "medium",
"rule": "HARDCODED_URL",
"message": "Hardcoded external URL detected in src/index.ts at line 42.",
"file": "src/index.ts",
"line": 42
}
],
"stages": {
"stage0": "ok",
"stage1": "ok",
"stage2": "ok",
"stage3": "ok",
"stage4": "ok",
"stage5": "ok"
}
}
Verdict rules
| Verdict | Condition |
|---|---|
PASS | Zero findings, or only informational notes. |
PASS_WITH_NOTES | Low-severity findings only. |
FLAGGED | 1–3 high-severity findings. |
FAIL | Any critical finding, or 4+ high-severity findings. |
Pipeline stages
| Stage | Name | What it checks |
|---|---|---|
stage0 | Ingest | Tarball structure, SHA-512 hashing, extraction safety (no symlinks, no path traversal, no absolute paths) |
stage1 | Structure | Required files (SKILL.md, manifest), file count (under 100), total size (under 50 MB) |
stage2 | Static | AST analysis — eval, exec, obfuscated code, suspicious imports |
stage3 | Injection | Prompt injection patterns in Markdown and skill definition files |
stage4 | Secrets | Hardcoded credentials, API keys, tokens using entropy analysis |
stage5 | Supply chain | Dependency tree analysis, known-malicious package hashes |
Each stage is independent. A failure in one stage does not prevent subsequent stages from running. All findings are aggregated into the final verdict.
CLI Auth Flow
The CLI uses a browser-based OAuth flow to obtain an API key without requiring users to copy-paste tokens manually. The three-step sequence is: start → (user authorizes in browser) → exchange.
POST /api/v1/cli-auth/start — Begin OAuth flow
Generates a short-lived poll token and constructs the browser authorization URL.
Authentication: Not required.
Request body
{ "deviceName": "MacBook Pro (work)" }
Response 200 OK
{
"pollToken": "poll_01HXYZ...",
"authUrl": "https://tankpkg.dev/api/v1/cli-auth/authorize?token=poll_01HXYZ...",
"expiresIn": 300
}
The authUrl should be opened in the user's browser. expiresIn is in seconds (5 minutes). After this window the poll token is invalidated and the flow must be restarted.
Example
curl -X POST https://tankpkg.dev/api/v1/cli-auth/start \
-H "Content-Type: application/json" \
-d '{"deviceName": "MacBook Pro (work)"}'
GET /api/v1/cli-auth/authorize — Grant access
This URL is opened in the user's browser (not called directly by the CLI). The user is shown a consent screen, authenticates with GitHub if not already signed in, and approves the token issuance.
Query parameters
| Parameter | Description |
|---|---|
token | The pollToken from the start response. |
After the user approves, the browser is redirected back to the CLI callback and the poll token transitions to an authorized state. The CLI can now exchange it.
POST /api/v1/cli-auth/exchange — Exchange for API key
Exchanges an authorized poll token for a permanent API key. The CLI polls this endpoint after opening the browser URL, backing off until the user completes the authorization or the token expires.
Authentication: Not required.
Request body
{ "pollToken": "poll_01HXYZ..." }
Response 200 OK (authorized)
{
"apiKey": "tank_xxxxxxxxxxxxxxxxxxxxxxxx",
"scopes": ["skills:read", "skills:publish"]
}
Response 202 Accepted (pending — user has not yet authorized)
{ "status": "pending" }
Response 410 Gone (expired or already consumed)
{ "error": "POLL_TOKEN_EXPIRED", "message": "The poll token has expired or already been used." }
Poll tokens have a 5-minute TTL and are single-use. Once exchanged successfully, the token is invalidated. The CLI (`tank login`) handles polling and backoff automatically.
Example
# Poll until authorized (simplified — real CLI uses backoff)
curl -X POST https://tankpkg.dev/api/v1/cli-auth/exchange \
-H "Content-Type: application/json" \
-d '{"pollToken": "poll_01HXYZ..."}'
Badge
GET /api/v1/badge/:name — Audit score badge
Returns an SVG badge displaying a skill's current audit score. Designed to embed directly in GitHub READMEs and documentation.
Authentication: Not required.
Path parameters
| Parameter | Description |
|---|---|
name | Skill name, URL-encoded (e.g. @vercel%2Fnext-skill). |
Response
Returns image/svg+xml content. The badge color encodes the score tier:
| Score | Color | Meaning |
|---|---|---|
| 9–10 | Green | Excellent |
| 7–8 | Yellow-green | Good |
| 5–6 | Yellow | Fair |
| 3–4 | Orange | Poor |
| 0–2 | Red | Failing |
Usage in Markdown

Example
curl https://tankpkg.dev/api/v1/badge/%40vercel%2Fnext-skill -o badge.svg
Full Publish Workflow
The following shows the complete three-step flow the tank publish CLI command performs internally.
# Step 1 — Initiate publish, get a pre-signed upload URL
RESPONSE=$(curl -s -X POST https://tankpkg.dev/api/v1/skills \
-H "Authorization: Bearer tank_xxx" \
-H "Content-Type: application/json" \
-d '{
"manifest": {
"name": "@acme/my-skill",
"version": "1.2.0",
"description": "Does something useful.",
"visibility": "public",
"permissions": {}
}
}')
UPLOAD_URL=$(echo $RESPONSE | jq -r '.uploadUrl')
SKILL_ID=$(echo $RESPONSE | jq -r '.skillId')
VERSION_ID=$(echo $RESPONSE | jq -r '.versionId')
# Step 2 — Upload the tarball to the pre-signed URL
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/gzip" \
--data-binary @my-skill-1.2.0.tgz
# Step 3 — Compute SHA-512 and confirm
INTEGRITY="sha512-$(openssl dgst -sha512 -binary my-skill-1.2.0.tgz | base64)"
curl -X POST https://tankpkg.dev/api/v1/skills/confirm \
-H "Authorization: Bearer tank_xxx" \
-H "Content-Type: application/json" \
-d "{
\"skillId\": \"$SKILL_ID\",
\"versionId\": \"$VERSION_ID\",
\"integrity\": \"$INTEGRITY\"
}"