Skip to content

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:

Authorization: Bearer repod_xxxxxxxxxx

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:

{ "access_token": "eyJhbGci...", "token_type": "bearer" }


GET /auth/me

Return the currently authenticated user's profile.

curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/auth/me

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).

curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/auth/users

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).

curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/packages/

GET /artifacts/

List all artifacts with enriched metadata (version history, security status).

curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/artifacts/

GET /artifacts/{name}

Full detail for a package: all versions, last validation results, CVE findings.

curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/artifacts/nginx

GET /artifacts/{name}/dependencies

Resolve a package's declared dependencies against the internal pool.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8000/artifacts/nginx/dependencies

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.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  http://localhost:8000/artifacts/admin/sync-index

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 "file=@mypackage_1.0.0_amd64.deb" \
  -F "distribution=jammy" \
  http://localhost:8000/upload/
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.

curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/import/sources

POST /import/sources

Maintainer+. Add an external repository as an import source.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  http://localhost:8000/import/sources \
  -d '{
    "name": "ubuntu-focal",
    "type": "apt",
    "url": "http://archive.ubuntu.com/ubuntu",
    "distribution": "focal",
    "components": ["main", "universe"]
  }'
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  http://localhost:8000/import/sources \
  -d '{
    "name": "epel9",
    "type": "rpm",
    "url": "https://dl.fedoraproject.org/pub/epel/9/Everything/x86_64/"
  }'

POST /import/sources/{id}/sync

Maintainer+. Synchronize the package index from a source (does not download packages — updates the searchable catalog only).

curl -X POST -H "Authorization: Bearer $TOKEN" \
  http://localhost:8000/import/sources/1/sync

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.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  http://localhost:8000/import/packages \
  -d '{
    "source_id": 1,
    "package": "nginx",
    "version": "1.24.0-1",
    "arch": "amd64",
    "distribution": "jammy"
  }'
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  http://localhost:8000/import/packages \
  -d '{
    "source_id": 2,
    "package": "nginx",
    "version": "1.24.0-1.el9.ngx",
    "arch": "x86_64",
    "distribution": "almalinux9"
  }'

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.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8000/security/clamav/status

POST /security/clamav/update

Admin only. Force an immediate ClamAV signature database update.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  http://localhost:8000/security/clamav/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.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  http://localhost:8000/api/v1/distributions/init

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.

curl -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8000/downloads/stats?days=30"

Dashboard (/dashboard)

GET /dashboard/summary

Reader+. Aggregated dashboard metrics: package count, pending reviews, CVE findings, recent activity.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8000/dashboard/summary

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.

curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/health | jq .
{
  "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

Upload a package in CI
#!/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 .
.github/workflows/publish.yml
- 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"