OncePad for developers

Create one-time secrets from scripts, CI jobs, and agents — the API, the encryption format, and a working example

Short answer: OncePad is a free, no-account service for sharing a password, secret, or file through an end-to-end encrypted link. The link contains no decryption key, works once, and expires after 24 hours. Files may be up to 25 MB.

How it fits together

The server never sees plaintext, so the API never accepts it. Your code encrypts the secret under a passphrase, posts the ciphertext, and gets back an id. The recipient opens /s/<id> in an ordinary browser, types the passphrase, and the browser decrypts it — exactly as if the secret had been created on the website. There are no API keys and no accounts.

  1. Generate a passphrase: seven or more distinct words from the EFF Short Wordlist 2.0, lowercase, joined by single spaces. The reveal page only accepts words from that list, so a phrase built any other way cannot be typed in.
  2. Encrypt with suite 0x02 (below) and base64url-encode the blob without padding.
  3. POST /api/secrets. Send the link on one channel and the passphrase on another.

The envelope (suite 0x02)

blob = 0x02 ‖ salt(16) ‖ iv(12) ‖ AES-256-GCM ciphertext ‖ tag(16)
key  = PBKDF2-HMAC-SHA256(passphrase_utf8, salt, 600000 iterations, 32 bytes)
aad  = 0x02 0x02 ‖ salt ‖ iv

Salt and IV are fresh random bytes for every secret. The passphrase is encoded as UTF-8 with no Unicode normalization. The normative definition is the encryption format specification, with conformance test vectors to check an implementation against. The browser's own library is oncepad-crypto.js.

Endpoints

RequestBodyResponse
POST /api/secrets{"blob": "<base64url>", "ttl": 3600} — ttl in seconds is optional, clamped to 60–86,400200 {"id", "mgmt_token"}
GET /api/secrets/<id>—200 {"kind": "text", "blob"} exactly once, then 410. This read destroys the secret.
POST /s/<id>/burn{"mgmt_token": "…"}200 — revoked before anyone opened it; 403 for a wrong token

The ciphertext may be up to 64 KB (about 64 KB of plaintext minus 45 bytes of envelope). Errors: 400 for an empty, oversized, or non-base64url blob; 413 for a request body over ~100 KB; 429 with Retry-After when one address sends too many requests; 503 when the service is at capacity. File secrets use a separate upload flow and are not covered here.

Never put a /api/secrets/<id> URL anywhere a bot might fetch it: that request is the reveal. The /s/<id> link you share is safe — opening it only shows the passphrase prompt.

A complete example (Node.js, no dependencies)

This script reads a secret from standard input, encrypts it, and prints the link on stdout and the passphrase on stderr. It uses only the WebCrypto API built into Node 20+, which is the same code path the browser uses. Download oncepad-send.mjs.

// oncepad-send.mjs — create a OncePad secret from a script or CI job. Node 20+, no dependencies.
//
//   curl -O https://www.eff.org/files/2016/09/08/eff_short_wordlist_2_0.txt
//   printf '%s' "$DB_PASSWORD" | node oncepad-send.mjs
//
// Prints the link on stdout and the passphrase on stderr: send them on two different channels.
import { readFileSync } from "node:fs";

const BASE = "https://oncepad.com";
const WORDS = readFileSync("eff_short_wordlist_2_0.txt", "utf8")
  .trim()
  .split("\n")
  .map((line) => line.split("\t")[1]);

// A uniform index in [0, n): rejection sampling, so no word is more likely than another.
function randIndex(n) {
  const limit = 2 ** 32 - (2 ** 32 % n);
  const buf = new Uint32Array(1);
  do crypto.getRandomValues(buf);
  while (buf[0] >= limit);
  return buf[0] % n;
}

// Seven DISTINCT list words, lowercase, single spaces (~72 bits). The reveal page only accepts list
// words, so a phrase built any other way could not be typed in.
function passphrase(n = 7) {
  const words = new Set();
  while (words.size < n) words.add(WORDS[randIndex(WORDS.length)]);
  return [...words].join(" ");
}

// Suite 0x02: blob = 0x02 | salt(16) | iv(12) | AES-256-GCM(ciphertext + tag),
// key = PBKDF2-HMAC-SHA256(passphrase, salt, 600000), AAD = 0x02 0x02 | salt | iv.
async function seal(plaintext, phrase) {
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const material = await crypto.subtle.importKey("raw", new TextEncoder().encode(phrase), "PBKDF2", false, ["deriveKey"]);
  const key = await crypto.subtle.deriveKey(
    { name: "PBKDF2", hash: "SHA-256", salt, iterations: 600000 },
    material,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt"],
  );
  const aad = new Uint8Array([0x02, 0x02, ...salt, ...iv]);
  const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad }, key, plaintext);
  return new Uint8Array([0x02, ...salt, ...iv, ...new Uint8Array(ct)]);
}

const phrase = passphrase();
const blob = await seal(readFileSync(0), phrase);
const res = await fetch(`${BASE}/api/secrets`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ blob: Buffer.from(blob).toString("base64url") }),
});
if (!res.ok) throw new Error(`OncePad answered ${res.status}`);
const { id } = await res.json();

console.log(`${BASE}/s/${id}`);
console.error(`passphrase: ${phrase}`);

In CI, pipe the secret in from your secrets store (never from a command-line argument, which lands in process listings and shell history) and deliver the two lines through two different systems — for example, the link in the ticket and the passphrase in a direct message.

Agents and automation

The same flow lets an automated agent hand a credential to a person without the value ever appearing in a chat transcript, a log, or a pull request: the agent encrypts locally, posts the blob, and reports only the link, while the passphrase travels to the human separately. Because nothing in the request is readable, the API itself never holds a usable secret.

Fair use

The API is free and keyless, protected by per-address rate limits and the acceptable-use terms. It is a hand-off, not storage: a secret lives at most 24 hours and only in memory. If you need guarantees beyond that, store the secret in a secrets manager and use OncePad only to deliver it.