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:

  1. The client fills in a masked input field on the portal
  2. 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
  3. 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
  4. The private key exists only in the server environment (never in the database)
  5. 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:

json
{
  "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:

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:

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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

View the audit log (dashboard session only, account owner — an API key cannot call this endpoint):

GET /v1/audit?action=secret.revealed

There 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