Skip to content

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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Kecuali endpoint public (/public/*, /health, /auth/*).

Auth

Register

POST /auth/register

Body:

{
"email": "[email protected]",
"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/login

Body (dual-mode — identifier bisa email atau username):

{ "identifier": "[email protected]", "password": "...", "totp_code": "123456" }

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 email masih di-accept sebagai fallback untuk client lama, akan di-remove di v0.4. Migrate ke identifier.

Google OAuth

GET /auth/google/start

Redirect 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 /me

Response:

{
"user_id": "...", "email": "...", "is_admin": false,
"email_verified": true, "has_team": true, "totp_enabled": false
}

My teams

GET /teams

Response:

{ "teams": [ { "id": "uuid", "slug": "acme-corp", "name": "Acme", "plan": "starter" } ] }

Secrets

List secrets

GET /teams/:teamId/secrets?environment=production&search=stripe

Query params (optional):

  • environment — filter env
  • search — 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/:secretId

Response:

{ "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=production
  • format: dotenv (default) atau json
  • environment: 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/secrets

Body:

{
"name": "DATABASE_URL",
"type": "credential",
"value": "postgres://...",
"environment": "production",
"description": "Production database"
}

Update secret

PUT /teams/:teamId/secrets/:secretId

Body 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/:secretId

Butuh 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/bulk

Body:

{
"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-dotenv

Body:

{
"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/bulk

Body:

{ "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-env

Body:

{ "ids": ["uuid-1", "uuid-2"], "environment": "production" }

Pindahkan banyak secret ke env baru sekaligus (mis. promote stagingproduction 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 /tokens

Create token

POST /tokens

Body:

{ "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/:tokenId
POST /teams/:teamId/secrets/:secretId/shares

Body:

{ "max_views": 1, "ttl_hours": 24 }

Response:

{ "share": { ... }, "url": "https://calvery.xyz/share/shr_xxx" }

Public view (anonymous)

GET /public/share/:token

Tiap 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=0

Role 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 invalid
  • 401 — token invalid/expired atau password salah
  • 403 — tidak punya permission (role tidak cukup)
  • 404 — resource tidak ditemukan
  • 409 — 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.