# API reference and CLI

Canonical URL: https://burnthesecret.com/api-docs
Markdown URL: https://burnthesecret.com/api-docs.md

# API Documentation

Integrate Burn the Secret into your applications to create secure, self-destructing secret links programmatically.

### Base URL

`https://burnthesecret.com/api`

### AES-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_key`

Create 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

New

Prefer 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_... --json
```

Free 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

POST`/secrets`Auth Optional

Create 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
  }
}
```

GET`/secrets/:key`

Check 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
}
```

POST`/secrets/:secretKey`

Retrieve 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.

DELETE`/secrets/:key`

Permanently 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=decryptionKey`

#### Server-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;
```
