API reference

V-Secrets API

Standard REST over HTTPS. One header for authentication, JSON in and out, nothing to install. Everything the console does is available here.

Base URLhttps://api.vsecrets.dev/api/v1

Quickstart

From an empty workspace to reading an encrypted secret. Create the project and runtime key in the console, then everything else is API.

Store a secret

curl -X POST https://api.vsecrets.dev/api/v1/projects/$PROJECT_ID/secrets \
  -H "X-API-Key: $VSECRETS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "DATABASE_URL",
    "value": "postgresql://user:pass@host:5432/db",
    "description": "Primary database"
  }'

Read it back

Reveal returns the decrypted value and writes an audit entry recording which credential asked for it.

curl -X POST \
  https://api.vsecrets.dev/api/v1/projects/$PROJECT_ID/secrets/DATABASE_URL/reveal \
  -H "X-API-Key: $VSECRETS_KEY"
{
  "key": "DATABASE_URL",
  "value": "postgresql://user:pass@host:5432/db",
  "version": 1,
  "updated_at": "2026-08-21T14:22:07Z"
}
Fetch once at boot. Read secrets at startup and hold them in memory. Calling reveal per request is slower and floods your audit log with noise you'll have to scroll past during an actual incident.

Authentication

Two credentials, for two different situations.

Runtime keys

For services, CI pipelines and scripts. Send as X-API-Key. Scopeable to a single project, revocable at any time, and never expiring unless you give them a date.

curl https://api.vsecrets.dev/api/v1/projects \
  -H "X-API-Key: vsec_live_3loFSNE1..."

Bearer tokens

For user sessions in the console. Short-lived, refreshed automatically. You generally won't use these directly.

curl https://api.vsecrets.dev/api/v1/users/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
The raw key is shown once. At creation and never again — the database stores an HMAC-SHA256 hash, not the key. Lost it? Rotate rather than creating a new one, so the audit trail keeps the connection.

Scopes

Runtime keys carry explicit permissions. The read/reveal split is the one worth understanding: secrets:read returns names and metadata, while secrets:reveal is what actually decrypts a value.

projects:readList projects and read their metadata
projects:writeCreate and update projects
secrets:readList secret keys — values stay hidden
secrets:revealDecrypt and return secret values
secrets:writeCreate and update secrets
secrets:deletePermanently remove secrets
api_keys:writeIssue and revoke other runtime keysA key with this scope can mint itself broader access. Grant it only to administrative tooling — revoking the original won't contain a leak.

Common combinations

Inventoryprojects:read secrets:readDashboards and drift checks — sees what exists, decrypts nothing.
Runtime+ secrets:revealWhat a deployed service needs, and nothing more.
Deploy+ secrets:writeProvisioning tooling and CI, not a running app.

Projects

Projects group secrets and are the unit of cryptographic isolation — each has its own derived encryption key, so compromising one doesn't expose another.

GET
/projects
List every project in the workspace
POST
/projects
Create a project
PUT
/projects/{id}
Update name, description or environment
curl -X POST https://api.vsecrets.dev/api/v1/projects \
  -H "X-API-Key: $VSECRETS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "checkout-production",
    "description": "Payment service credentials",
    "environment": "production"
  }'

Secrets

Values are encrypted with AES-256-GCM before reaching the database. Each ciphertext is bound to its project, key name and version — moving encrypted data between projects makes decryption fail rather than silently succeed.

GET
/projects/{id}/secrets
List secret metadata — values are never returned here
POST
/projects/{id}/secrets
Store a new secret
POST
/projects/{id}/secrets/{key}/reveal
Decrypt and return a value, writing an audit entry

Versioning

Updating a secret creates a new version rather than overwriting. Rotations stay reversible, and the audit trail keeps the history intact.

Runtime keys

GET
/api-keys
List keys with prefixes, scopes and status
POST
/api-keys
Issue a key — the raw value is returned once
POST
/api-keys/{id}/rotate
Replace a key, with a grace period
DELETE
/users/me/api-keys/{id}
Revoke immediately
curl -X POST https://api.vsecrets.dev/api/v1/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "checkout-service",
    "project_id": "b3f2...",
    "scopes": ["projects:read", "secrets:read", "secrets:reveal"],
    "expires_in_days": 90
  }'

Rotating a key

Rotation issues a replacement with identical scopes and project binding, and keeps the old key working for a grace period. Your services pick up the new value on their own deploy cycle instead of needing a coordinated cutover.

curl -X POST https://api.vsecrets.dev/api/v1/api-keys/$KEY_ID/rotate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"grace_period_hours": 24}'
{
  "new_key": {
    "id": "9f1c...",
    "api_key": "vsec_live_3loFSNE1...",
    "key_prefix": "vsec_live_3loFSNE1"
  },
  "old_key_prefix": "vsec_live_yCknU6v3",
  "grace_expires_at": "2026-08-22T14:22:07Z",
  "message": "Key rotated. The previous key keeps working until..."
}
Rotate on suspicion, not on schedule. This is incident response — a leaked key, a departed contractor, a repository that went public. Once the grace period closes, anything still using the old key stops authenticating.

Set grace_period_hours to 0 to revoke the old key immediately. Use that when the key is known to be compromised and you'd rather take the outage than leave it live.

Audit logs

Every read, write and reveal is recorded with the credential that made it, the source address, and the outcome.

GET
/audit-logs?limit=100
Recent activity, newest first
{
  "action": "POST",
  "resource_type": "secret",
  "request_path": "/projects/b3f2.../secrets/STRIPE_KEY/reveal",
  "api_key_id": "9f1c...",
  "ip_address": "203.0.113.42",
  "status_code": 200,
  "created_at": "2026-08-21T14:22:07Z"
}

When api_key_id is null, the request came from a user session rather than a runtime key.

Error handling

Errors return a JSON body with a detail field describing what went wrong.

{
  "detail": "Secret with key 'DATABASE_URL' already exists"
}
400Malformed body, or a duplicate key in the project
401Missing, expired or revoked credentialAlso returned when a rotated key is used after its grace period ended.
402Plan limit reachedThe request was valid and authorized — the account needs a higher plan.
403Valid credential, but scoped elsewhere
404Project or secret doesn't exist in this workspace

Rate limits

Limits scale with plan. Exceeding them returns 429 — back off and retry.

Free100 requests/minute
Pro1,000 requests/minute
Business2,500 requests/minute
You shouldn't be near these. Fetching secrets at boot means a handful of calls per deploy. Hitting a rate limit usually means something is calling reveal in a request handler.