API Documentation
Integrate Burn the Secret into your applications to create secure, self-destructing secret links programmatically.
Base URL
https://burnthesecret.com/apiAES-256-GCM Encryption
Industry-standard authenticated encryption used by banks and governments.
Zero-Knowledge Architecture
Browser and CLI encryption keeps keys off our servers. Plaintext API submissions use server-side encryption.
Self-Destructing Links
Set view limits and expiration times. Links stop working at their view limit or expiry.
RESTful JSON API
Simple HTTP endpoints with consistent response formats.
Authentication
Authentication is optional. Anyone can create a secret without an API key—sending a key only attributes the secret to your account. To attribute a secret, include your key in the Authorization header:
Authorization: Bearer ks_live_your_api_keyCreate API keys in your Dashboard → API Access. If you omit the header the request is still accepted (anonymous, or linked to your signed-in browser session). Only if you send a key that is invalid or revoked is the request rejected—with a 401 INVALID_API_KEY response.
Quick Start
Create a secret link with a single API call:
curl -X POST https://burnthesecret.com/api/secrets \
-H "Authorization: Bearer ks_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"text": "my-sensitive-password", "ttl": 86400, "maxViews": 1}'Command-Line Interface
NewPrefer the terminal? The burnthesecret CLI creates one-time links without leaving your shell. It encrypts on your machine (AES-256-GCM) and sends only the ciphertext — the decryption key stays in the link fragment, so it stays zero-knowledge, exactly like the web app. No dependencies; requires Node.js 18+.
# One-off, no install (Node.js 18+)
npx burnthesecret "my secret"
# From a pipe — great for CI and scripts
echo "$DB_PASSWORD" | npx burnthesecret --ttl 3600 --views 1
# Require a passphrase, or attribute to your account with an API key
npx burnthesecret "token" --passphrase "over-the-phone"
npx burnthesecret "token" --api-key ks_live_... --jsonFree and open source (MIT). It prints a single link — share that. Anyone with the full link can read the secret once, so treat the link like the secret.
Endpoints
/secretsAuth OptionalCreate a new secret link. Use one encryption mode for every item: either plaintext text and base64 data-URL files for server-side encryption, or client-encrypted text and files using the same AES key. Mixed modes are rejected. Client ciphertext must be canonical base64 with a 12-byte base64 IV. The browser and CLI preserve exact text, including whitespace.
Request Body
| Parameter | Type | Description |
|---|---|---|
| text | string | object | Secret content (2 MB combined plaintext; 4 MB encoded JSON maximum). A plain string (server encrypts it—not zero-knowledge) or {ciphertext, iv} you encrypted client-side (zero-knowledge) |
| ttl | number | Time to live in whole seconds. 1800 (30 min) to 7776000 (90 days). Default: 86400 |
| maxViews | number | null | Max views (integer 1-999) or null for unlimited views until expiry. Default: 1 |
| passphrase | string | Optional raw passphrase, 4-256 chars (send over HTTPS; hashed server-side with a salted KDF) |
| files | array | Array (max 20) of files with filename, mimeType, data |
Response
{
"success": true,
"secretUrl": "https://burnthesecret.com/secret/Kj8mNp2x...#key=...",
"metadataUrl": "https://burnthesecret.com/receipt/Yz3nLq9b...",
"metadata": {
"key": "Yz3nLq9bFs7hGc...",
"secretKey": "Kj8mNp2xQr5tVw...",
"expiresAt": "2026-01-27T12:00:00.000Z",
"maxViews": 1
}
}/secrets/:keyCheck if a secret exists and get metadata. Does not consume a view.
{
"exists": true,
"type": "secret",
"hasPassphrase": false,
"expiresAt": "2026-01-27T12:00:00.000Z",
"maxViews": 1,
"viewsRemaining": 1
}/secrets/:secretKeyRetrieve secret content. This consumes one view.
Request Body (if passphrase protected)
{ "passphrase": "your-passphrase" }Response
{
"success": true,
"text": {
"ciphertext": "base64-encoded...",
"iv": "base64-encoded..."
},
"viewsRemaining": 0
}Decrypt using the key from the URL fragment with AES-256-GCM.
/secrets/:keyPermanently delete (burn) a secret.
curl -X DELETE https://burnthesecret.com/api/secrets/Yz3nLq9bFs7hGc...
Encryption
Burn the Secret uses AES-256-GCM, the same encryption used by TLS 1.3, Signal, and government systems.
Zero-Knowledge Architecture
The decryption key is stored in the URL fragment (after #), which browsers never send to servers. We only store encrypted ciphertext—useless without the key. Decryption happens entirely in the recipient's browser.
URL Structure:
https://burnthesecret.com/secret/secretKey#key=decryptionKeyServer-Side Encryption
Send plaintext (or the legacy secret field)—we encrypt it server-side and return the URL with the key in the fragment. This path is not zero-knowledge: the server briefly holds the key.
Client-Side Encryption
Encrypt before sending as {ciphertext, iv}—you control the key. True zero-knowledge, maximum security.
Rate Limits
A baseline limit of 1,000 requests per day per IP and operation applies before authentication. Signed-in creation also allows 100/hour and 1,000/day per account. API keys share 1,000/hour per owning account. Protected retrieval allows five attempts/minute per secret and IP, including empty attempts. Limits fail closed if their service is unavailable. Content limits are uniform: 2 MB combined text/files, 4 MB encoded JSON, 90 days, 999 views (or unlimited until expiry), and 20 files.
Response Headers
X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 950 X-RateLimit-Reset: 1706360400
Errors
All errors return a consistent JSON structure:
{
"error": "Invalid passphrase",
"code": "INVALID_PASSPHRASE",
"hint": "Include the correct passphrase in your request.",
"requestId": "req_abc123..."
}| Status | Code | Description |
|---|---|---|
| 200 | — | Success |
| 400 | INVALID_* | Bad request (invalid params) |
| 401 | INVALID_API_KEY | A key was sent but is invalid or revoked (a key is optional; omitting it is fine) |
| 403 | INVALID_PASSPHRASE / DELETION_NOT_ALLOWED | Wrong passphrase, or burn attempted on a secret whose sender disabled deletion |
| 404 | NOT_FOUND | Secret not found |
| 410 | SECRET_EXPIRED / SECRET_BURNED / MAX_VIEWS_REACHED | Secret is gone: expired, burned, or max views reached |
| 429 | RATE_LIMIT_EXCEEDED / TOO_MANY_ATTEMPTS | Rate limit exceeded, or too many wrong passphrase attempts |
| 500 | INTERNAL_ERROR | Server error |
Unknown keys and secrets removed by cleanup return 404 NOT_FOUND. Retrieval may return 410 while an expired, burned, or exhausted status row still exists. Access ends at expiry; hourly cleanup removes expired rows and attachments from the active database, normally within one hour. New secrets last at most 90 days; existing secrets retain their saved expiry dates.
Security Best Practices
Choose the Right Encryption Mode
- Server-side: Convenient—we handle encryption. Plaintext briefly in memory.
- Client-side: Maximum security—you control the key entirely.
Passphrase Tips
Use 8+ characters with mixed case, numbers, symbols. Share passphrases via a different channel than the URL.
Minimize Exposure
Use the shortest TTL and lowest maxViews needed. For one-time credentials, use maxViews: 1.
Code Examples
Decrypting Retrieved Secrets
JavaScript
const urlKey = window.location.hash.split('key=')[1];
const rawKey = Uint8Array.from(
atob(urlKey.replace(/-/g, '+').replace(/_/g, '/')),
c => c.charCodeAt(0)
);
const key = await crypto.subtle.importKey(
'raw', rawKey, { name: 'AES-GCM' }, false, ['decrypt']
);
const ciphertext = Uint8Array.from(atob(response.text.ciphertext), c => c.charCodeAt(0));
const iv = Uint8Array.from(atob(response.text.iv), c => c.charCodeAt(0));
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv }, key, ciphertext
);
const secret = new TextDecoder().decode(plaintext);Python
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64
key = base64.urlsafe_b64decode(url_key + '==')
ciphertext = base64.b64decode(response['text']['ciphertext'])
iv = base64.b64decode(response['text']['iv'])
aesgcm = AESGCM(key)
plaintext = aesgcm.decrypt(iv, ciphertext, None)
secret = plaintext.decode('utf-8')Client-Side Encryption
For maximum security, encrypt before sending to Burn the Secret:
JavaScript
// Generate key and IV
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
// Encrypt
const plaintext = new TextEncoder().encode('my-secret');
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv }, key, plaintext
);
// Export key for URL
const rawKey = await crypto.subtle.exportKey('raw', key);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)));
// Send to API
const response = await fetch('https://burnthesecret.com/api/secrets', {
method: 'POST',
headers: {
'Authorization': 'Bearer ks_live_...',
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: {
ciphertext: btoa(String.fromCharCode(...new Uint8Array(ciphertext))),
iv: btoa(String.fromCharCode(...iv))
}
})
});
const data = await response.json();
const fullUrl = data.secretUrl + '#key=' + keyBase64;