Secrets Vault
Overview
The secret item type lets you collect credentials — passwords, API keys, hosting logins, third-party service tokens — from clients with encryption at rest. The decrypted plaintext can be revealed exactly once, through either of two paths: your agent calling get_intake_results (MCP or GET /v1/intakes/:id/results), or the account owner clicking Reveal on the item in the dashboard (POST /v1/intakes/:id/items/:key/reveal). Both draw on the same one-time reveal — whichever happens first gets the plaintext, and the other side gets back a note that it was already revealed.
Secrets vault is available on Solo, Studio, and Agency plans. Attempting to include a secret item on a Free plan returns plan_required.
Why secrets are different
Regular items (text, file, color, URL) store their submitted values in plaintext in the database. That is appropriate for non-sensitive content.
Secret items work differently:
- The client fills in a masked input field on the portal
- The value travels over HTTPS and is encrypted on arrival using a libsodium sealed box with BriefGate's public key, before anything is written to the database; it is never stored in plaintext
- Only the ciphertext is stored in the database — the plaintext exists in server memory just long enough to be sealed, and again only during the one-time reveal; it is never written to disk or logs
- The private key exists only in the server environment (never in the database)
- Even a complete database dump cannot reveal secret values
The private key is a 32-byte libsodium keypair secret held in the server environment.
Declaring a secret item
Include it in items[] when calling define_intake:
{
"key": "wp_admin",
"type": "secret",
"label": "WordPress admin credentials",
"help": "Your username and password. Encrypted at rest, revealed only once to your developer."
}On the portal, the client sees:
- A masked password input, with a show/hide toggle
- A lock icon
- The note: "Encrypted — shown once to your provider only."
There is no way to recover the value through the portal after submission.
One-time reveal flow
Step 1. There are three ways to trigger the reveal:
- An agent calls
get_intake_results(MCP, orGET /v1/intakes/:id/resultswith an API key that has thesecrets:readoradminscope). This reveals every secret item on the intake in one call. - The account owner clicks Reveal on a single credential in the dashboard, which calls
POST /v1/intakes/:id/items/:key/reveal(dashboard session only, owner role required). This reveals one item. - The account owner (or a
secrets:read/adminAPI key) downloads the intake's ZIP withinclude_secrets=true(GET /v1/intakes/:id/download). This reveals every unrevealed secret item and includes the values inpodklady.pdf— see Download Everything as a ZIP.
All three draw on the same one-time reveal recorded on the secret itself: whichever happens first gets the plaintext, and the others are told it was already revealed. The dashboard's own confirmation dialog says this outright before the owner clicks Reveal: "Credentials are shown once. After this it cannot be retrieved again — by you, or by an agent using an API key."
On the first call, via either path, the response includes the decrypted plaintext.
Via get_intake_results / GET /v1/intakes/:id/results, the item's entry in results
looks like:
{
"wp_admin": {
"value": "admin:MyPassword123",
"one_time": true,
"first_reveal": true,
"expires_at": "2026-08-15T10:00:00Z"
}
}Via the dashboard's POST /v1/intakes/:id/items/:key/reveal, the response is:
{
"key": "wp_admin",
"value": "admin:MyPassword123",
"first_reveal": true,
"expires_at": "2026-08-15T10:00:00Z"
}Step 2. Store the value in your own secrets manager. This is your only chance, from either path.
That is meant literally: the value is gone the moment it has been read, no matter which endpoint read it. There is one narrow allowance — the same caller (the same API key, or the same owner's dashboard session) may repeat its own call within five minutes and get the same value back, so a connection dropped between our response and your process does not destroy the credential. The retry window is tracked per caller identity, so an API key and a dashboard reveal never share it with each other — if the agent revealed it via the API, the owner clicking Reveal five seconds later still gets "already revealed," and vice versa. Any other caller, or the same caller later, gets nothing.
On a subsequent get_intake_results call, the item drops out of results entirely (its
value is null) and meta explains why:
{
"wp_admin": {
"secret_unavailable": true,
"reason": "Already revealed — secrets are one-time and cannot be shown again.",
"revealed_at": "2026-07-16T09:14:02Z"
}
}A subsequent dashboard reveal instead gets:
{
"key": "wp_admin",
"value": null,
"already_revealed": true,
"revealed_at": "2026-07-16T09:14:02Z",
"message": "This credential was already revealed. Secrets are shown once and cannot be shown again."
}If you lost the value, the way back is the same as if it had expired: ask the
client for it again with request_revision.
If the secret expires (30 days after submission, configurable per deployment via
SECRETS_TTL_DAYS) before anyone collects it:
Call request_revision(intake_id, "wp_admin", "Credentials expired before we could retrieve them. Please resubmit.") to ask the client to enter them again.
The API key used with get_intake_results needs the secrets:read or admin scope to
reveal secret values. Without it, GET /v1/intakes/:id/results still succeeds — every
other item comes back normally — but each secret item is dropped from results and
meta marks it secret_unavailable: true, reason: "Scope 'secrets:read' or 'admin' required." instead.
Audit trail
Every reveal is logged with:
- Actor — the API key that called
get_intake_results(actor_type: "api_key") or the dashboard owner who clicked Reveal (actor_type: "user") - IP address of the caller
- Timestamp
intake_idanditem_key, nested undermeta
View the audit log (dashboard session only, account owner — an API key cannot call this endpoint):
GET /v1/audit?action=secret.revealedThere is no server-side filter for a single intake — intake_id is not a supported query
parameter. Each entry's meta.intake_id and meta.item_key tell you which item it was, so
filter client-side if you need just one intake's history.
This log is append-only and cannot be deleted.
Auto-expiry
Secrets expire 30 days after the client submits the item.
An expired secret has value: null and cannot be retrieved. Use request_revision to ask the client to resubmit.
Best practices for agents
Store immediately. When get_intake_results returns a secret with first_reveal: true, store the value before the call returns. It is never available again through the API.
Store in a secrets manager. After revealing, put the value in your secrets manager (1Password, Vault, AWS Secrets Manager, etc.). Do not write it to a file, log it, or include it in a commit.
Never log the revealed value. Even at debug level. If your logger captures variables, mask secrets before passing them to any logging function.
Rotate after use. Once you have finished using a credential (e.g., after deploying the site), inform the client that they can rotate it. The captured credential was valid at collection time; rotating it limits exposure if the value was ever stored unsafely.
Availability
| Plan | Secrets vault |
|---|---|
| Free | Not available — plan_required error |
| Solo | Available |
| Studio | Available |
| Agency | Available |