Home›Secreta›API
Secreta · API

Secreta API, as shipped in the download

Secreta REST API, MCP & Webhooks (v3.0)

Secreta's API is deliberately one endpoint. The shape below is dictated by the product's zero-knowledge contract, not by convention — please read this section before asking where the rest is.

There is no endpoint that lists or reads secrets

A one-time secret exists for exactly one retrieval by exactly one recipient. An endpoint that enumerated outstanding secrets would hand anyone with a leaked API key a map of every live token. So there is no GET /api/secrets, no GET /api/secrets/{id}, and **no way to read a secret's content through this API at all**. Retrieval happens exactly once, in a browser, at the share link — and burns the secret.

The API takes ciphertext, never plaintext

Secreta's confidentiality comes from an AES-256-GCM key generated in the browser that travels only in the URL fragment (after #). Browsers never send a fragment to a server, so **Secreta's server cannot decrypt anything.** An API that accepted plaintext would have to encrypt server-side, putting the key on the server and silently downgrading every secret created that way.

So: you encrypt, you keep the key, you post the ciphertext.

Authentication

Authorization: Bearer apk_...

Created in Settings → API & Webhooks; shown once, stored hashed.

POST /api/secrets

curl -X POST https://secrets.example.com/api/secrets \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"ciphertext":"<base64 AES-256-GCM blob>","iv":"<base64 12-byte iv>",
       "expiry_hours":24,"max_views":1}'
{ "id": 41, "token": "k3Jf...", "share_url": "https://secrets.example.com/s/k3Jf...",
  "expires_at": "2026-08-07 09:00:00", "max_views": 1, "created_at": "2026-08-06 09:00:00",
  "note": "Append \"#\" + your base64url key to share_url. The server never receives the key and cannot rebuild this link." }

share_url is deliberately incomplete. It has no # fragment because the server has never seen your key. Append it yourself:

FULL_LINK="${share_url}#${your_base64url_key}"

If the response ever did contain a working link, that would mean the server held the key — and the product would be broken. A test in the suite asserts the returned URL contains no #.

expiry_hours accepts 0, 1, 6, 24, 72, 168, 720; max_views is clamped to 1–100 — the same bounds the browser UI enforces. An API caller gets no wider limits than a person.

Posting something that is not base64 is rejected with 422, because the likely mistake is pasting plaintext, and storing that would hand you a "secret" that was never encrypted.

A file with the secret (3.2.0, when an administrator has switched files on). Encrypt the file with the same key under its own 12-byte nonce and send "file_ciphertext" (base64 of ciphertext ‖ tag) and "file_iv"; put the file's name inside the encrypted text, e.g. {"v":1,"note":"...","file":{"name":"server.key"}}, so the server never sees it. The response says "has_file": true. A file sharing the text's nonce, one too short to be AES-GCM output, or one over the server's cap is refused with 422.

Who it is for (3.3.0, optional). Send "label": "Alex (new starter)" to name the person a link is for; it is shown on the team's own Secrets list, returned as label in the response, never sent to the person opening the link, clipped to 120 characters and cleared once the link is 60 days old (the sweep runs when a secret is made and when Data requests is opened). Never put the secret in it. To send one secret to several people, encrypt it once per person (a key each) and post each with its own label.

From a terminal: app/bin/secreta-send in your download does all of this for you and prints the full link: SECRETA_URL=https://secrets.example.com SECRETA_KEY=apk_... secreta-send < password.txt.

Webhooks — metadata only

EventFires when
secret.createdA secret is stored — from the share page, the REST API or /mcp
secret.viewedA recipient opens it
secret.burnedThe final view consumes it, or someone on your team burns it from Secrets (then views_left is 0)

Payloads carry id, views_used, max_views, views_left, expires_at and passphrase_protected — and nothing else. There is no ciphertext, no iv and no key in a webhook, exactly as there is none in the audit log. A suite test asserts this.

Changed in 3.0: the token is no longer in the payload. It used to be, under a comment that said "metadata only" — but a token is the delivery credential, and for a multi-view secret secret.viewed fires while the link is still live, so the old payload posted a working share link to a third-party URL. Correlate on id instead.

X-Secreta-Event: secret.burned
X-Secreta-Signature: sha256=<hmac_sha256(raw_body, your_webhook_secret)>

MCP — the endpoint for AI agents (new in 3.0)

Secreta speaks the Model Context Protocol at POST /mcp, stateless streamable-HTTP. This is the shared best agent story: an agent that has to hand a human a credential can mint a one-time link instead of pasting the credential into a chat transcript that will be logged, summarized and retained.

Authentication is the same API key as the REST surface, and it carries the same role. GET /mcp answers 405 with Allow: POST rather than 404, so a probing client can tell the difference between "wrong method" and "no MCP support here".

ToolReads or writesWhat it does
create_secretWRITESMint a one-time link from ciphertext you have already encrypted. Returns share_url without the key fragment — append # + your base64url key yourself.
statsREAD-ONLYCounts only: created, live, burned, expired-unread, views, and the policy in force. No tokens.

There is deliberately no read_secret tool, and that is a feature. Reading a secret burns it. An agent that fetched one would consume the recipient's only view and destroy the delivery it was asked to perform — and it would do so silently, leaving the human with a dead link and no explanation. Asking for read_secret returns Unknown tool. If an agent needs to know something, give it the value directly; Secreta exists to move a value to a person, once.

The tool takes ciphertext, never plaintext, for the same reason the REST endpoint does: a tool that accepted plaintext would have to encrypt server-side, which would put the key on the server and silently downgrade every secret created that way. Sending something that is not base64 is refused with a message that says exactly that.

Connect it to an assistant

Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

The addresshttps://secrets.example.com/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP over streamable HTTP, stateless

Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:

claude mcp add --transport http secreta https://secrets.example.com/mcp \
  --header "Authorization: Bearer apk_xxxx"

ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):

{
  "type": "mcp",
  "server_label": "secreta",
  "server_url": "https://secrets.example.com/mcp",
  "authorization": "apk_xxxx",
  "require_approval": "never"
}

**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"*. Pasted as-is, a block written for another client will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.

A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

curl -X POST https://secrets.example.com/mcp \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Secreta implements the protocol, not an integration with a particular vendor.

What this product deliberately does NOT have

  • No email notifications. Secreta sends no mail, so 2.0 adds no SMTP settings. A zero-knowledge tool should not carry a mail credential it never uses.
  • No CSV import. There is nothing to import: a secret is generated, shared once, and destroyed. Bulk-loading historical secrets would mean holding plaintext to encrypt it — the exact thing this product exists to avoid.

Backup

Changed in 3.0. 2.0 exported every table with nothing redacted, reasoning that the stored secrets are already double-encrypted. That is true of the secrets — and it was false of everything around them: the same file carried users.password_hash, api_keys.token_hash, users.totp_secret and every webhook signing secret in clear text. Those are redacted now, and restoring onto a working install keeps the values that install already holds.

What deliberately stays is the secrets table — ciphertext, IV and token — because that is the data a restore exists to bring back, and it is useless without the browser key this server has never held. So be plain about what the file is: **a disaster-recovery artifact, not one to hand to a contractor**, because it contains live share tokens. What it cannot contain is any secret's plaintext. The suite proves that property survives a full backup→restore cycle by decrypting a restored secret with the original browser key.

Connect it to Zapier, Make or n8n

Secreta talks to other tools without a plugin: it sends a webhook (an HTTPS message) the moment something happens, and it accepts calls to the API above. Zapier, Make and n8n can use both.

Before you start

  • In Secreta, open Settings → API & Webhooks. Mint an API key (choose Read only if the other tool only reads), and add a webhook when you have its URL (below).
  • Use your install's full address, for example https://secreta.yourcompany.com.

When something happens in Secreta: send it to another tool

  1. In the other tool, create the trigger that gives you a URL to receive webhooks: - Zapier: the "Webhooks by Zapier" app, trigger Catch Hook (Webhooks by Zapier needs a paid Zapier plan). - Make: module Webhooks → Custom webhook. - n8n: the Webhook node.
  2. Copy that URL into Secreta under Settings → API & Webhooks, and choose the event or events below.
  3. Make the event happen once in Secreta, so the other tool receives a sample and you can map its fields.
EventFires when
secret.createdA secret is stored — from the share page, the REST API or /mcp
secret.viewedA recipient opens it
secret.burnedThe final view consumes it, or someone on your team burns it from Secrets (then views_left is 0)

Every delivery carries X-Secreta-Signature, an HMAC-SHA256 signature over the raw body. Zapier and Make accept the delivery without checking it, so keep the webhook URL private. In n8n you can check it in a Code node before using the data.

When something happens elsewhere: act in Secreta

  • Zapier: "Webhooks by Zapier", action POST (or Custom Request).
  • Make: module HTTP → Make a request.
  • n8n: the HTTP Request node.

Send the request to your install's address plus the path from the Endpoints table above, with the header Authorization: Bearer <your API key> and a JSON body. A read-only key cannot write.

The endpoints that change something:

  • POST /api/secrets

Example recipes

  • secret.created → a row in Google Sheets: webhook trigger on secret.created (A secret is stored — from the share page, the REST API or /mcp), then the Google Sheets "Create row" action with the fields you want.
  • secret.viewed → a message in Slack or Teams: webhook trigger on secret.viewed (A recipient opens it), then the Slack or Microsoft Teams "Send message" action.
  • Creating a secret from another tool is not a plain recipe: the API takes only ciphertext (see above), so the other tool must encrypt in the browser or in a script first. Use the share page, or a script you control, for that.

These recipes use only the events and endpoints documented above. We have not run them inside Zapier, Make or n8n ourselves; Secreta's own automated tests check its API.

← Back to Secreta · Manual · Quickstart · Test run