REST API Reference
Base URL: https://api.calvery.xyz/api/v1
Self-host: ganti base sesuai deployment kamu (contoh https://vault.internal/api/v1).
Semua endpoint butuh header:
Authorization: Bearer cvsm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxContent-Type: application/jsonKecuali endpoint public (/public/*, /health, /auth/*).
Auth
Register
POST /auth/registerBody:
{ "username": "renzy_dev", "password": "min-8-chars", "name": "Nama Lengkap Anda", "newsletter_opt_in": true}Field:
username— 3-40 karakter, alphanumeric + underscore, case-insensitive unique. Dipakai untuk login dual-mode.name— nama lengkap untuk display + audit log. Bebas (tidak wajib real name).newsletter_opt_in— opsional, default false. Kalau true, user auto-subscribe newsletter tanpa butuh double-opt-in email.
Response 201:
{ "token": "eyJhbG...", "user": { "id": "...", "email": "...", "username": "renzy_dev", "name": "..." } }Login
POST /auth/loginBody (dual-mode — identifier bisa email atau username):
Atau:
{ "identifier": "renzy_dev", "password": "..." }totp_code cuma wajib kalau 2FA aktif. Kalau belum input tapi 2FA on, response 401 dengan "error": "2fa_required" — frontend prompt user input TOTP lalu retry.
Legacy: field
identifier.
Google OAuth
GET /auth/google/startRedirect ke Google consent. Setelah user approve, Google redirect ke /auth/google/callback dengan authorization code → backend tukar jadi JWT → redirect ke calvery.xyz/auth/oauth-callback#token=....
Me & Teams
Me
GET /meResponse:
{ "user_id": "...", "email": "...", "is_admin": false, "email_verified": true, "has_team": true, "totp_enabled": false}My teams
GET /teamsResponse:
{ "teams": [ { "id": "uuid", "slug": "acme-corp", "name": "Acme", "plan": "starter" } ] }Secrets
List secrets
GET /teams/:teamId/secrets?environment=production&search=stripeQuery params (optional):
environment— filter envsearch— ILIKE match di name
Response:
{ "secrets": [ { "id": "...", "name": "DATABASE_URL", "type": "credential", "environment": "production", "description": "", "updated_at": "..." } ], "total": 1}Get secret (dengan value)
GET /teams/:teamId/secrets/:secretIdResponse:
{ "secret": { "id": "...", "name": "...", ... }, "value": "plaintext-value" }Tiap call ini di-log di audit log (action: read).
Export (semua secret as .env / JSON)
GET /teams/:teamId/secrets/export?format=json&environment=productionformat:dotenv(default) ataujsonenvironment: filter env
JSON format:
{ "DATABASE_URL": "postgres://...", "STRIPE_KEY": "sk_..." }SDK semua bahasa pakai endpoint ini internal untuk getAll() + cache lokal.
Create secret
POST /teams/:teamId/secretsBody:
{ "name": "DATABASE_URL", "type": "credential", "value": "postgres://...", "environment": "production", "description": "Production database"}Update secret
PUT /teams/:teamId/secrets/:secretIdBody sama dengan create (tanpa name — nama tidak bisa diubah). Value lama disimpan di secret_versions untuk rollback.
Delete secret (soft delete)
DELETE /teams/:teamId/secrets/:secretIdButuh role Admin+. Data tetap di DB tapi deleted_at di-set — tidak bisa di-restore via API (butuh intervensi admin DB).
Bulk create secrets
POST /teams/:teamId/secrets/bulkBody:
{ "items": [ { "name": "DATABASE_URL", "value": "postgres://...", "environment": "production" }, { "name": "API_KEY", "value": "sk-...", "environment": "production", "type": "api_key" } ]}Response:
{ "results": [ { "index": 0, "id": "uuid", "name": "DATABASE_URL", "ok": true }, { "index": 1, "name": "API_KEY", "ok": false, "error": "secret 'API_KEY' di env 'production' sudah ada" } ], "summary": { "total": 2, "ok": 1, "failed": 1 }}Maksimal 500 item per request. Per-item sukses/gagal di-report; tidak atomic (item yg sukses tetap ter-insert walau yg lain fail). Plan limit max_secrets diperiksa di awal — kalau existing + items.length > max, full request ditolak (tidak partial insert).
Bulk import dari format .env
POST /teams/:teamId/secrets/bulk/import-dotenvBody:
{ "content": "DATABASE_URL=postgres://...\nAPI_KEY=\"sk-abc\"\n# ini komentar, di-skip\nexport SMTP_HOST=smtp.gmail.com", "environment": "production"}Parser support: comment (#), blank line, export prefix, quoted value ("..." / '...'), escaped \n/\t/\" di double-quoted. Tidak support heredoc / variable expansion. Max content 200KB.
Response sama dengan bulk create + field parsed (jumlah KEY yang match regex).
Bulk delete secrets
DELETE /teams/:teamId/secrets/bulkBody:
{ "ids": ["uuid-1", "uuid-2", "uuid-3"] }Butuh role Admin+ (destructive op at scale). Scope otomatis ke team_id dari URL — ID dari team lain silently di-skip (tidak ada info leak). Semua yang lolos di-delete soft (set deleted_at).
Bulk move environment
POST /teams/:teamId/secrets/bulk/move-envBody:
{ "ids": ["uuid-1", "uuid-2"], "environment": "production" }Pindahkan banyak secret ke env baru sekaligus (mis. promote staging → production setelah rilis). Kalau nama secret sudah ada di env target, item itu skip dengan error; secret lain tetap ter-pindah.
Access Tokens
List tokens
GET /tokensCreate token
POST /tokensBody:
{ "name": "github-actions", "expires_at": "2027-01-01T00:00:00Z" }Response satu-satunya kesempatan untuk lihat plain token:
{ "api_token": { "id": "...", "name": "...", "token_prefix": "cvsm_xxxxx...", ... }, "token": "cvsm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }Revoke token
DELETE /tokens/:tokenIdShare Links (v0.3+)
Create share link
POST /teams/:teamId/secrets/:secretId/sharesBody:
{ "max_views": 1, "ttl_hours": 24 }Response:
{ "share": { ... }, "url": "https://calvery.xyz/share/shr_xxx" }Public view (anonymous)
GET /public/share/:tokenTiap call decrement view count. Response:
{ "secret_name": "...", "value": "plaintext", "view_count": 1, "max_views": 1, "expires_at": "..." }Audit
Get audit logs
GET /teams/:teamId/audit?limit=50&offset=0Role Admin+. Response log immutable:
{ "logs": [ { "action": "read", "resource": "secret", "user_email": "...", "ip_address": "...", "created_at": "..." } ] }Errors
Semua error JSON:
{ "error": "pesan user-friendly dalam Bahasa Indonesia" }HTTP codes:
400— input invalid401— token invalid/expired atau password salah403— tidak punya permission (role tidak cukup)404— resource tidak ditemukan409— conflict (e.g. email sudah terdaftar, slug dipakai)429— rate limit (auth endpoint 10 req/min, API 60 req/min)500— server error
Rate limits
Default:
/auth/*→ 10 request/menit per IP/api/v1/*(authenticated) → 60 request/menit per token/public/*→ 30 request/menit per IP
Exceed → 429 dengan header Retry-After: <seconds>.
OpenAPI spec
OpenAPI 3.0 YAML tersedia di: api.calvery.xyz/openapi.yaml (planned v0.4).
Sementara, pakai SDK kami yang sudah abstraksi — atau generate client dari examples di atas.