API Reference¶
Repod exposes a REST API at http://YOUR_HOST:8000. All endpoints (except
public ones listed below) require a bearer token.
Base URL
All paths are relative to the backend API base URL. If you deploy behind a
reverse proxy with an /api/ prefix, prepend /api to every path listed here.
Example: GET /artifacts/ → GET https://repo.example.com/api/artifacts/
Authentication¶
JWT (interactive sessions)¶
# Obtain a JWT token
TOKEN=$(curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin1234!"}' \
| jq -r .access_token)
# Use the token
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/packages/
Tokens expire after 60 minutes (JWT_EXPIRE_MINUTES in backend.env).
API tokens (CI/CD)¶
For non-interactive pipelines, use a permanent API token:
API tokens are created via POST /auth/api-tokens (admin only). The plaintext
token is returned exactly once.
Public endpoints (no authentication required)¶
| Endpoint | Description |
|---|---|
POST /auth/token |
Log in — obtain a JWT |
POST /auth/forgot-password |
Request a password reset |
POST /auth/reset-password |
Reset a password with a one-time token |
GET /auth/roles |
List available roles |
GET /health |
Full health check |
GET /health/live |
Liveness probe |
GET /health/ready |
Readiness probe |
HTTP status codes¶
| Code | Meaning |
|---|---|
200 OK |
Success |
201 Created |
Resource created |
400 Bad Request |
Invalid parameters or body |
401 Unauthorized |
Token missing, invalid, or expired |
402 Payment Required |
Enterprise-only feature |
403 Forbidden |
Insufficient role |
404 Not Found |
Resource not found |
409 Conflict |
Resource already exists |
424 Failed Dependency |
Index not yet populated — run a sync first |
429 Too Many Requests |
Rate limit exceeded |
500 Internal Server Error |
Server-side error |
Auth (/auth)¶
POST /auth/token¶
Public. Authenticate and obtain a JWT. Tries local accounts first, then LDAP if enabled.
curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin1234!"}'
Response:
GET /auth/me¶
Return the currently authenticated user's profile.
Response:
{
"username": "admin",
"role": "admin",
"full_name": "Administrator",
"email": "[email protected]",
"active": true,
"last_login": "2026-05-15T10:23:41+00:00"
}
POST /auth/change-password¶
Change the currently authenticated user's own password.
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
http://localhost:8000/auth/change-password \
-d '{"current_password":"Admin1234!","new_password":"NewPass5678!"}'
Password policy: minimum 8 characters, at least one uppercase letter, at least one digit or special character.
GET /auth/users¶
Admin only. List all users (password hashes are never included).
POST /auth/users¶
Admin only. Create a new user.
{
"username": "jsmith",
"password": "SecurePass1!",
"role": "uploader",
"full_name": "John Smith",
"email": "[email protected]"
}
Roles: admin | maintainer | uploader | auditor | reader
PATCH /auth/users/{username}¶
Admin only. Update a user's role, full name, email, or active status.
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
http://localhost:8000/auth/users/jsmith \
-d '{"role":"maintainer","active":true}'
An admin cannot change their own role.
DELETE /auth/users/{username}¶
Admin only. Delete a user. An admin cannot delete their own account.
POST /auth/users/{username}/reset-password¶
Admin only. Force-set a new password for another user.
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
http://localhost:8000/auth/users/jsmith/reset-password \
-d '{"new_password":"TempPass9!"}'
POST /auth/api-tokens¶
Admin only. Create a permanent API token. The plaintext token is returned only in this response.
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
http://localhost:8000/auth/api-tokens \
-d '{"name":"gitlab-ci","role":"uploader","expires_days":365}'
Response:
{
"token": "repod_xxxxxxxxxx",
"message": "Copy this token now — it will not be shown again.",
"prefix": "repod_"
}
expires_days is optional — omit for a non-expiring token.
DELETE /auth/api-tokens/{token_id}¶
Admin only. Revoke an API token immediately.
Packages & Artifacts (/packages, /artifacts)¶
GET /packages/¶
List packages from the internal pool (raw pool format).
GET /artifacts/¶
List all artifacts with enriched metadata (version history, security status).
GET /artifacts/{name}¶
Full detail for a package: all versions, last validation results, CVE findings.
GET /artifacts/{name}/dependencies¶
Resolve a package's declared dependencies against the internal pool.
Response includes all_satisfied, missing list, and install_blocked flag.
DELETE /artifacts/{name}¶
Maintainer+. Delete all versions of a package.
DELETE /artifacts/{name}/{version}¶
Maintainer+. Delete a specific version.
GET /artifacts/audit/logs¶
Auditor+. Retrieve audit log entries.
| Parameter | Type | Description |
|---|---|---|
page |
int | Page number (default: 1) |
per_page |
int | Entries per page (default: 100) |
package |
string | Filter by package name |
action |
string | UPLOAD, DELETE, LOGIN, SECURITY_DECISION, etc. |
result |
string | SUCCESS, FAILURE, WARNING |
q |
string | Free-text search across package, user, and detail fields |
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8000/artifacts/audit/logs?action=UPLOAD&q=nginx"
POST /artifacts/admin/sync-index¶
Maintainer+. Rebuild the package index from manifests on disk.
Upload (/upload)¶
POST /upload/¶
Uploader+. Upload a package. Triggers the full security pipeline (antivirus → CVE scan → GPG verification → distribution publish).
curl -X POST -H "Authorization: Bearer $TOKEN" \
-F "[email protected]_64.rpm" \
-F "distribution=almalinux9" \
http://localhost:8000/upload/
The endpoint returns a pipeline result summary:
{
"status": "published",
"package": "mypackage",
"version": "1.0.0",
"distribution": "jammy",
"pipeline": {
"antivirus": "clean",
"cve_scan": "no_findings",
"gpg": "signed",
"dependencies": "satisfied"
}
}
Possible status values: published, pending_review, quarantined, rejected.
Import (/import)¶
GET /import/sources¶
Maintainer+. List configured external import sources.
POST /import/sources¶
Maintainer+. Add an external repository as an import source.
POST /import/sources/{id}/sync¶
Maintainer+. Synchronize the package index from a source (does not download packages — updates the searchable catalog only).
The response is a Server-Sent Events (SSE) stream — suitable for real-time
progress display. Each event has data: {"status":"...", "count":N}.
POST /import/packages¶
Maintainer+. Import (download + validate) a specific package from an indexed source.
Security (/security)¶
GET /security/packages¶
Maintainer+. List packages with security findings.
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8000/security/packages?status=pending_review"
Filter parameters: status (pending_review, quarantined, published),
severity (critical, high, medium, low).
POST /security/packages/{name}/{version}/decide¶
Maintainer+. Approve or reject a package in the CVE review queue.
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
http://localhost:8000/security/packages/nginx/1.24.0/decide \
-d '{
"decision": "approve",
"justification": "CVE-2024-1234 does not apply to our deployment (no affected feature used)"
}'
decision: approve | reject
POST /security/packages/{name}/{version}/rescan¶
Maintainer+. Re-run the CVE scan against the current Grype database.
curl -X POST -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/security/packages/nginx/1.24.0/rescan
GET /security/clamav/status¶
Maintainer+. Check ClamAV daemon status and signature database version.
POST /security/clamav/update¶
Admin only. Force an immediate ClamAV signature database update.
GET /security/policy¶
Admin only. Retrieve the current CVE response policy configuration.
PATCH /security/policy¶
Admin only. Update CVE response policies.
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
http://localhost:8000/security/policy \
-d '{"critical": "block", "high": "review", "medium": "warn", "low": "allow"}'
SBOM (/sbom)¶
GET /sbom/{name}/{version}¶
Auditor+. Download the SBOM for a specific package version.
# CycloneDX JSON
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8000/sbom/nginx/1.24.0?format=cyclonedx&arch=amd64" \
-o nginx-sbom.cdx.json
# SPDX JSON
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8000/sbom/nginx/1.24.0?format=spdx" \
-o nginx-sbom.spdx.json
format: cyclonedx | spdx
GET /sbom/export¶
Auditor+. Export the full repository SBOM.
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8000/sbom/export?format=cyclonedx" \
-o repod-full.sbom.cdx.json
Distributions (/distributions)¶
POST /distributions/init¶
Admin only. Initialize or re-initialize all distributions.
POST /distributions/promote¶
Maintainer+. Promote a package from one distribution to another without re-uploading.
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
http://localhost:8000/api/v1/distributions/promote \
-d '{"package":"nginx","from_dist":"jammy","to_dist":"noble"}'
from_dist and to_dist: one of jammy, noble, focal, bookworm
from_dist and to_dist: one of almalinux9, rocky9, fedora, etc.
POST /distributions/migrate¶
Maintainer+. Migrate all packages from one distribution to another.
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
http://localhost:8000/api/v1/distributions/migrate \
-d '{"from_dist":"focal","to_dist":"jammy"}'
Warning
The source distribution is not removed — packages exist in both after migration.
Downloads (/downloads)¶
GET /downloads/stats¶
Maintainer+. Package download statistics parsed from Nginx access logs.
Dashboard (/dashboard)¶
GET /dashboard/summary¶
Reader+. Aggregated dashboard metrics: package count, pending reviews, CVE findings, recent activity.
Settings (/settings)¶
GET /settings¶
Admin only. Retrieve all runtime settings. Sensitive values (LDAP bind
password, SMTP password) are masked as ***.
PATCH /settings¶
Admin only. Update runtime settings (LDAP, SMTP, webhook URL, CVE policy, retention, etc.).
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
http://localhost:8000/settings \
-d '{
"ldap_url": "ldaps://dc.example.com:636",
"ldap_bind_dn": "CN=repod-svc,OU=ServiceAccounts,DC=example,DC=com",
"ldap_bind_password": "service-account-password",
"ldap_base_dn": "OU=Users,DC=example,DC=com"
}'
Health (/health)¶
GET /health/live¶
Public. Liveness probe — returns {"status":"ok"} as long as the process
is running. Used by Docker health checks and load balancers.
GET /health/ready¶
Public. Readiness probe — checks that the database and critical services (ClamAV socket, Grype database) are ready to serve requests.
GET /health¶
Authenticated. Full health report with component-level status.
{
"status": "ok",
"version": "v1.2.0",
"components": {
"database": "ok",
"clamav": "ok",
"grype": "ok",
"gpg": "ok"
}
}
Rate limiting¶
| Endpoint group | Limit |
|---|---|
POST /auth/token |
10 req / min |
POST /upload/ |
20 req / min |
POST /import/* |
10 req / min |
POST /distributions/* |
5 req / min |
When a rate limit is exceeded, the API returns 429 Too Many Requests with a
Retry-After header indicating the seconds until the next request is allowed.
CI/CD integration examples¶
#!/usr/bin/env bash
set -euo pipefail
REPOD_URL="${REPOD_URL:-http://repod:8000}"
# Authenticate
TOKEN=$(curl -sf -X POST "$REPOD_URL/auth/token" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$REPOD_USER\",\"password\":\"$REPOD_PASSWORD\"}" \
| jq -r .access_token)
# Upload
curl -sf -X POST "$REPOD_URL/upload/" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@dist/mypackage_1.0.0_amd64.deb" \
-F "distribution=jammy" \
| jq .
- name: Publish package to Repod
env:
REPOD_URL: ${{ secrets.REPOD_URL }}
REPOD_USER: ci-uploader
REPOD_PASSWORD: ${{ secrets.REPOD_API_TOKEN }}
run: |
TOKEN=$(curl -sf -X POST "$REPOD_URL/auth/token" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$REPOD_USER\",\"password\":\"$REPOD_PASSWORD\"}" \
| jq -r .access_token)
curl -sf -X POST "$REPOD_URL/upload/" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@dist/mypackage_1.0.0_amd64.deb" \
-F "distribution=jammy"