Skip to content
Solo ProTeamBusinessEnterprise

Secrets & versioning

Reading and writing secret values on the machine data-plane, immutable versions, optimistic locking, and bulk reads.

Secret values live on the machine data-plane (/api/v1/secrets/**), reached with an Authorization: Bearer pv_live_… key. The console never returns values — humans manage access, machines read data.

Reading#

bash
# current version
curl -H "Authorization: Bearer pv_live_..." \
  https://api.pulsvault.com/api/v1/secrets/prod/product/web/db/password

# a specific version
curl -H "Authorization: Bearer pv_live_..." \
  "https://api.pulsvault.com/api/v1/secrets/prod/product/web/db/password?version=2"

The response includes path, value, versionNumber, contentType, createdAt, and non-secret metadata. Requires the read action.

Writing & versioning#

Every write creates a new immutable version — previous versions are retained for rollback and history.

bash
curl -X PUT -H "Authorization: Bearer pv_live_..." \
  -H "Content-Type: application/json" \
  -d '{"value":"new-secret"}' \
  https://api.pulsvault.com/api/v1/secrets/prod/product/web/db/password

Requires the write action. Values are limited to 1 MiB.

Optimistic locking#

To avoid clobbering a concurrent writer, pin the version with If-Match:

bash
curl -X PUT -H "If-Match: 4" ... # 412 if the current version isn't 4

The CLI's rotate uses this automatically.

Bulk reads#

Read up to 100 paths in one request (one audit entry, per-path errors):

bash
curl -X POST -H "Authorization: Bearer pv_live_..." \
  -H "Content-Type: application/json" \
  -d '{"paths":["acme-web/prod/product/web/db/password","acme-web/prod/product/web/stripe/key"]}' \
  https://api.pulsvault.com/api/v1/secrets/bulk

Bulk writes#

POST /secrets/bulk-write applies up to 100 upserts and deletes in one all-or-nothing transaction. Every path is validated and authorized first; if any entry fails, nothing is written and the response reports which one:

bash
curl -X POST -H "Authorization: Bearer pv_live_..." \
  -H "Content-Type: application/json" \
  -d '{"upserts":[{"path":"acme-web/prod/product/web/db/password","value":"…"}],
       "deletes":["acme-web/prod/product/web/db/legacy-password"]}' \
  https://api.pulsvault.com/api/v1/secrets/bulk-write

This is what makes a rotation safe: the new credential and the removal of the old one land together, so no deploy ever sees a half-applied change. The CLI's import batches through this endpoint.

Rotating (re-keying)#

POST /{project}/{env}/{path}/rotate appends a new version holding the same value encrypted under a fresh data key. The plaintext is decrypted and re-encrypted inside the server, so it never crosses the network:

bash
curl -X POST -H "Authorization: Bearer pv_live_..." \
  https://api.pulsvault.com/api/v1/secrets/acme-web/prod/product/web/db/password/rotate

Requires the rotate action (write also satisfies it). Accepts If-Match like a write, so a scheduled re-key cannot silently overwrite a concurrent update.

Deleting#

DELETE soft-deletes a secret. It stops answering reads at once — CI gets a 404 — but nothing is destroyed: the secret moves to the namespace's recycle bin, keeps its version history, and undelete brings it back for 30 days. A nightly job removes it after that, and the values are then unrecoverable. Requires the delete action, and so does restoring it — the permission that removed a secret is the one that puts it back.

Retention & destroying versions#

An actively rotated secret accumulates versions, so every namespace has a max versions per secret cap: each write prunes the oldest versions beyond it, never the current one.

The cap is resolved in three steps, and the first one that has a value wins:

  1. the namespace's own setting, on its overview page;
  2. your space's default version retention, under DevOps → Retention;
  3. your plan's ceiling, if neither is set.

Leaving a field empty means inherit the next step down — it does not mean unlimited. Version retention is never unlimited on any plan, because history beyond 100 versions cannot be listed by any client anyway.

Plan Max versions per secret
Developer 10
Team 25
Business 50
Enterprise 100

A value above your plan's ceiling is refused when you save it. If your plan later changes to a lower tier, your stored choice is kept but the lower ceiling applies — the screen shows you both numbers, and your original choice takes effect again if you upgrade.

Lowering a cap deletes ciphertext

Reducing a cap — at the namespace or as the space default — permanently destroys the versions past it. At the namespace you are shown exactly how many versions across how many secrets before it happens; the space default takes effect the next time each affected secret is written.

Destroy is not delete

A space administrator can destroy one non-current version's ciphertext from the console. The version disappears from the history immediately and can no longer be revealed or rolled back to, but the ciphertext is erased 30 days later — so destroying the wrong version is recoverable, while destroying a leaked one still ends in a real erase. To skip the window entirely, purge it. Both refuse the current version and are audited. Use them to remove a value that should never have been stored — retention caps handle routine cleanup.

Rate limits#

100 secrets/minute per key by default (configurable per key). The unit is a secret, not an HTTP call: a bulk read of 40 paths spends 40, and a bulk write of 30 upserts plus 10 deletes likewise spends 40. Responses carry X-RateLimit-Limit / -Remaining / -Reset; a 429 includes Retry-After.

The window slides over your last 60 seconds rather than resetting on the minute, so you cannot spend a whole allowance at second 59 and another at second 61 — and Retry-After is usually far shorter than a minute. Full detail on both budgets: API keys.

Prefer the CLI or an integration

You rarely need raw curl. The pulsvault CLI and the pipeline integrations handle auth, masking, and env injection.

Was this page helpful?