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#
# 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.
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/passwordRequires the write action. Values are limited to 1 MiB.
Optimistic locking#
To avoid clobbering a concurrent writer, pin the version with If-Match:
curl -X PUT -H "If-Match: 4" ... # 412 if the current version isn't 4The CLI's rotate uses this automatically.
Bulk reads#
Read up to 100 paths in one request (one audit entry, per-path errors):
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/bulkBulk 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:
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-writeThis 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:
curl -X POST -H "Authorization: Bearer pv_live_..." \
https://api.pulsvault.com/api/v1/secrets/acme-web/prod/product/web/db/password/rotateRequires 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:
- the namespace's own setting, on its overview page;
- your space's default version retention, under DevOps → Retention;
- 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.
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.
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.
You rarely need raw curl. The pulsvault CLI and the
pipeline integrations handle auth, masking, and env injection.