Documentation

REST API, CLI, SDKs, Chrome Extension, Telegram Bot, and MCP server. Everything you need to build on deniable encryption.

Quickstart

From zero to deniable encryption in under 5 minutes.

1. Get your free API key

Register here. It takes 10 seconds. You'll get a key like dk_abc123...

POST /api/register
curl -X POST https://deny.sh/api/register \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com"}'

2. Encrypt a message

curl -X POST https://deny.sh/api/encrypt \
  -H "Authorization: Bearer dk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "abandon abandon abandon ability abort about above absent absorb abstract",
    "password1": "real-pass",
    "password2": "control-pass"
  }'

# Returns: {"ciphertext": "a3f2...hex", "controlData": "b7c1...hex"}

3. Add a deniable decoy

Generate new control data that makes the same ciphertext decrypt to a completely different message.

curl -X POST https://deny.sh/api/deny \
  -H "Authorization: Bearer dk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ciphertext": "a3f2...hex",
    "password1": "real-pass",
    "password2": "control-pass",
    "fakeMessage": "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"
  }'

# Returns: {"controlData": "d4e5...hex"} (new control data for the decoy)

4. Decrypt with either password

Real message: use original control data23 lines
# Real message: use original control data
curl -X POST https://deny.sh/api/decrypt \
  -H "Authorization: Bearer dk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ciphertext": "a3f2...hex",
    "controlData": "b7c1...hex",
    "password1": "real-pass",
    "password2": "control-pass"
  }'
# "abandon abandon abandon ability abort about above absent absorb abstract"

# Decoy message: same ciphertext, different control data
curl -X POST https://deny.sh/api/decrypt \
  -H "Authorization: Bearer dk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ciphertext": "a3f2...hex",
    "controlData": "d4e5...hex",
    "password1": "real-pass",
    "password2": "control-pass"
  }'
# "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"

Same ciphertext. Different control data. Two completely different truths. Mathematically indistinguishable.

5. Or use the CLI instead

# Install
npm install -g deny-sh

# Encrypt
deny-sh encrypt -m "my secret" -p1 "real-pass" -p2 "control-pass" -o encrypted.dat

# Decrypt
deny-sh decrypt -i encrypted.dat -c control.dat -p1 "real-pass" -p2 "control-pass"

# Pipe from stdin
echo "my secret" | deny-sh encrypt -p1 "real-pass" -p2 "control-pass"

Authentication

All API endpoints (except registration and health) require a Bearer token:

Authorization: Bearer dk_your_api_key_here

Get your key from /register or via POST /api/register.


Get an API key

POST /api/register

{
  "email": "you@example.com",
  "name": "My App"          // optional
}

201
{
  "key": "dk_abc123...",
  "tier": "free",
  "monthly_limit": 500,
  "message": "Store your API key securely."
}

One key per email. If you've already registered, you'll get a 409. Store your key when you first receive it.


SDKs

All SDKs produce identical ciphertext. Encrypt in TypeScript, decrypt in Go. Full cross-language compatibility, verified with shared KAT (Known Answer Test) vectors.

TypeScript / Node.js

npm install deny-sh
"my secret"16 lines
import { encrypt, decrypt, generateControlData, generateDeniableControl } from 'deny-sh';

const plaintext = new TextEncoder().encode('my secret');
const controlData = generateControlData(plaintext.length + 4);
const { ciphertext } = encrypt(plaintext, {
  password1: 'real-pass',
  password2: 'control-pass',
  controlData
});

const { plaintext: decrypted } = decrypt(ciphertext, {
  password1: 'real-pass',
  password2: 'control-pass',
  controlData
});
console.log(new TextDecoder().decode(decrypted)); // "my secret"

Python

pip install deny-sh

Go

go get github.com/deny-sh-crypto/deny-go

Rust

cargo add deny-sh

Full SDK documentation and examples: deny.sh/sdks


POST Encrypt

Encrypt a message with two passwords. Returns ciphertext and control data (both hex-encoded).

POST /api/encrypt

{
  "message": "launch codes: 38.8977 N, 77.0365 W",
  "password1": "correct-horse",
  "password2": "battery-staple",
  "controlDataHex": "optional...hex"  // optional: supply your own control data
}

200
{
  "ciphertext": "a3f2...hex",
  "controlData": "b7c1...hex"
}
ParameterTypeDescription
message requiredstringPlaintext to encrypt. Max 10MB.
password1 requiredstringPrimary password. Max 1024 chars.
password2 requiredstringSecondary password. Max 1024 chars.
controlDataHexstringOptional hex-encoded control data. Auto-generated if omitted.

Store both the ciphertext and control data. You need the control data to decrypt or create decoys.


POST Decrypt

POST /api/decrypt

{
  "ciphertext": "a3f2...hex",
  "controlData": "b7c1...hex",
  "password1": "correct-horse",
  "password2": "battery-staple"
}

200
{
  "message": "launch codes: 38.8977 N, 77.0365 W"
}
ParameterTypeDescription
ciphertext requiredstringHex-encoded ciphertext.
controlData requiredstringHex-encoded control data.
password1 requiredstringPrimary password.
password2 requiredstringSecondary password.

POST Create decoy PAID

Generate new control data that makes the same ciphertext decrypt to a completely different message. Requires Dev or Pro plan.

POST /api/deny27 lines
POST /api/deny

{
  "ciphertext": "a3f2...hex",
  "password1": "correct-horse",
  "password2": "battery-staple",
  "fakeMessage": "grocery list: milk, eggs, bread"
}

200
{
  "controlData": "d4e5...hex"
}

// Decrypt the SAME ciphertext with the new control data:
POST /api/decrypt
{
  "ciphertext": "a3f2...hex",          // same ciphertext
  "controlData": "d4e5...hex",          // new control data
  "password1": "correct-horse",
  "password2": "battery-staple"
}

200
{
  "message": "grocery list: milk, eggs, bread"  // different truth
}
ParameterTypeDescription
ciphertext requiredstringHex-encoded ciphertext from a previous encrypt call.
password1 requiredstringPrimary password (same as used for encryption).
password2 requiredstringSecondary password (same as used for encryption).
fakeMessage requiredstringThe decoy plaintext. Must be shorter than or equal to the original message length.

POST Text encrypt NEW

Simplified text-only encryption. Returns hex strings directly (no binary). Control data is auto-generated.

POST /api/text/encrypt

{
  "message": "my secret",
  "password1": "real-pass",
  "password2": "control-pass"
}

200
{
  "ciphertextHex": "a3f2...hex",
  "controlData": "b7c1...hex"
}

POST Text decrypt NEW

Decrypt a text-encrypted message. Accepts both ciphertextHex and ciphertext field names.

POST /api/text/decrypt

{
  "ciphertextHex": "a3f2...hex",
  "controlData": "b7c1...hex",
  "password1": "real-pass",
  "password2": "control-pass"
}

200
{
  "message": "my secret"
}

POST Generate control data NEW

Generate random control data of a specific size. Useful when you want to manage control data separately from encryption.

POST /api/generate-control

{
  "size": 1024
}

200
{
  "controlData": "a1b2c3...hex"
}
ParameterTypeDescription
size requirednumberByte length of control data. 1 to 10,485,760 (10MB).

GET Check usage

GET /api/usage

200
{
  "tier": "free",
  "calls_this_month": 42,
  "limit": 500,
  "remaining": 458,
  "resets_at": "2026-05-01T00:00:00.000Z"
}

GET Health NO AUTH

Check if the API is running. No authentication required.

GET /api/health

200
{
  "status": "ok",
  "version": "1.0.0"
}

Vault PAID

Encrypted control file storage. Files are encrypted client-side before upload. Requires Dev or Pro plan.

POST /api/vault/store17 lines
POST /api/vault/store
{
  "label": "Main wallet backup",
  "encryptedData": "aabbccdd...hex",
  "iv": "1122...hex",
  "salt": "3344...hex"
}
201  { "id": "vi_abc123..." }

GET /api/vault/list
200  { "items": [{ "id", "label", "size", "created_at" }], "count": 1 }

GET /api/vault/:id
200  { "id", "label", "encryptedData", "iv", "salt", "size", "created_at" }

DELETE /api/vault/:id
200  { "deleted": true }

Vault limits: Free = 5 items, Dev = 100 items, Pro = 1,000 items. Max 1MB per item.


Dead man's switch PAID

Automatic release of encrypted data if you stop checking in. Set a schedule, add recipients, and check in periodically. Miss your deadline + grace period and recipients get emailed the encrypted payload.

POST Create a switch

POST /api/deadswitch
{
  "label": "Wallet backup",
  "encryptedData": "aabb...",
  "iv": "1122...",
  "salt": "5566...",
  "checkInDays": 30,
  "graceDays": 7
}
201  { "id": "ds_abc123..." }

encryptedData, iv, salt must be valid hex. checkInDays: 7-365. graceDays: 1-30.

Limits: Free = 1 switch / 2 recipients, Dev = 5 switches / 5 recipients, Pro = 20 switches / 10 recipients.

GET List switches

GET /api/deadswitch
200  { "switches": [...], "count": 1 }

GET Get switch details

GET /api/deadswitch/:id15 lines
GET /api/deadswitch/:id
200
{
  "id": "ds_abc123...",
  "label": "Wallet backup",
  "status": "active",
  "check_in_days": 30,
  "grace_days": 7,
  "last_check_in": 1775074685,
  "next_deadline": 1777666685,
  "grace_deadline": null,
  "recipients": [
    { "id": "sr_...", "email": "alice@example.com", "name": "Alice", "notified": 0 }
  ]
}

POST Check in

POST /api/deadswitch/:id/checkin
200  { "checked_in": true, "next_deadline": 1777666685 }

POST Pause / Resume

POST /api/deadswitch/:id/pause
200  { "paused": true }

POST /api/deadswitch/:id/resume
200  { "resumed": true, "next_deadline": ... }

POST Add recipient

POST /api/deadswitch/:id/recipients
{ "email": "alice@example.com", "name": "Alice" }
201  { "id": "sr_..." }

GET List recipients

GET /api/deadswitch/:id/recipients
200  { "recipients": [...] }

DEL Remove recipient

DELETE /api/deadswitch/:id/recipients/:rid
200  { "removed": true }

GET Check-in history

GET /api/deadswitch/:id/checkins
200  { "checkins": [{ "ip": "...", "timestamp": ... }] }

DEL Delete switch

DELETE /api/deadswitch/:id
200  { "deleted": true }

Status values: active (clock running), grace (missed deadline, grace window), triggered (released to recipients), paused (clock stopped).


Steganography API

Hide encrypted data inside PNG images server-side. Same algorithm as the browser tool.

POST Hide data

POST /api/stego/hide15 lines
POST /api/stego/hide
{
  "imageBase64": "iVBORw0KGgo...",  // Base64-encoded PNG
  "message": "your secret text",
  "password": "strong-password"
}

200
{
  "imageBase64": "iVBORw0KGgo...",  // Stego PNG with hidden data
  "width": 1000,
  "height": 1000,
  "payloadSize": 312,
  "capacityBytes": 374988
}

POST Extract data

POST /api/stego/extract
{
  "imageBase64": "iVBORw0KGgo...",  // Stego PNG
  "password": "strong-password"
}

200
{
  "message": "your secret text",
  "payloadSize": 312
}

POST Check capacity

POST /api/stego/capacity
{ "width": 1000, "height": 1000 }

200
{ "capacityBytes": 374988, "capacityKB": 366 }

Audit log PRO

Full audit trail of every API operation. Pro plan only.

GET /api/audit
200
{
  "entries": [
    { "action": "vault.store", "resource": "vi_abc123", "ip": "...", "timestamp": ... },
    { "action": "encrypt", "resource": "", "ip": "...", "timestamp": ... }
  ]
}

Managed Vault API

Enterprise-grade vault with higher limits, audit trails, and region selection. Business ($199/mo) and Enterprise ($999/mo) tiers.

POST Create vault

POST /api/managed-vault
{ "tier": "business", "label": "Production keys" }

201
{ "id": "mv_abc123...", "tier": "business", "max_items": 10000 }

GET List vaults

GET /api/managed-vault
200
{ "vaults": [...], "count": 2 }

POST Store item

POST /api/managed-vault/:vid/items
{
  "label": "BTC cold wallet",
  "encryptedData": "aabbcc...",
  "iv": "001122...",
  "salt": "334455...",
  "region": "eu-west"          // optional, default eu-west
}

201
{ "id": "mvi_def456..." }

GET List items

GET /api/managed-vault/:vid/items
200
{ "items": [{ "id": "mvi_...", "label": "...", "size": 256, "region": "eu-west", "created_at": ... }], "count": 5 }

GET Get item

GET /api/managed-vault/:vid/items/:iid
200
{ "id": "mvi_...", "label": "...", "encryptedData": "...", "iv": "...", "salt": "...", "size": 256, "region": "eu-west", "created_at": ... }

DEL Delete item

DELETE /api/managed-vault/:vid/items/:iid
200
{ "deleted": true }

GET Audit log

GET /api/managed-vault/:vid/audit
200
{
  "entries": [
    { "action": "item.store", "resource": "mvi_abc123", "ip": "...", "timestamp": ... }
  ]
}

POST Create checkout session NEW

Generate a Stripe checkout URL for upgrading to a paid plan.

POST /api/billing/checkout

{
  "plan": "dev",                              // "dev", "pro", "dev-annual", "pro-annual"
  "successUrl": "https://deny.sh/docs",       // optional
  "cancelUrl": "https://deny.sh/pricing"      // optional
}

200
{
  "url": "https://checkout.stripe.com/c/pay/..."
}

POST Customer portal NEW

Generate a link to the Stripe customer portal for managing subscriptions, invoices, and payment methods.

POST /api/billing/portal

200
{
  "url": "https://billing.stripe.com/p/session/..."
}

GET List plans NEW

Get all available billing plans with pricing.

GET /api/billing/plans

200
{
  "plans": [
    { "id": "dev", "name": "deny.sh Dev", "price": "$99/mo", "interval": "month", "calls": 10000 },
    { "id": "pro", "name": "deny.sh Pro", "price": "$199/mo", "interval": "month", "calls": 100000 },
    { "id": "dev-annual", "name": "deny.sh Dev (Annual)", "price": "$374/yr", "interval": "year", "calls": 10000 },
    { "id": "pro-annual", "name": "deny.sh Pro (Annual)", "price": "$948/yr", "interval": "year", "calls": 100000 }
  ]
}

CLI NEW

Zero-dependency command-line tool. Runs entirely offline. No API key needed for local operations.

Install

npm install -g deny-sh

Commands

Encrypt a message37 lines
# Encrypt a message
deny-sh encrypt -m "my secret" -p1 "pass1" -p2 "pass2" -o encrypted.dat

# Decrypt
deny-sh decrypt -i encrypted.dat -c control.dat -p1 "pass1" -p2 "pass2"

# Create a deniable decoy
deny-sh deny -i encrypted.dat -p1 "pass1" -p2 "pass2" -m "fake message" -o decoy-control.dat

# Protect a seed phrase (interactive)
deny-sh protect

# Encrypt .env files
deny-sh env protect .env
deny-sh env restore .env.deny

# Local vault (SQLite, never leaves your machine)
deny-sh vault set my-key "my-value"
deny-sh vault get my-key
deny-sh vault list
deny-sh vault delete my-key

# Generate random control data
deny-sh generate -s 1024 -o control.dat

# Run verification suite
deny-sh verify

# Check installation status
deny-sh status

# Initialize .deny/ directory in your project
deny-sh init

# Pipe from stdin
echo "my secret" | deny-sh encrypt -p1 "pass1" -p2 "pass2"
cat secrets.json | deny-sh encrypt -p1 "pass1" -p2 "pass2" -o secrets.enc

.deny/ directory convention

Run deny-sh init in any project to create a .deny/ directory. It auto-generates a .gitignore entry so your encrypted files never get committed. Control files, encrypted backups, and vault data all live here.

Full CLI docs: deny.sh/cli


Chrome Extension NEW

Encrypt and decrypt directly in your browser. Right-click any text to encrypt it in-place.

Features

  • Popup with Encrypt, Decrypt, and Vault tabs
  • Right-click context menu for in-page encryption
  • Shadow DOM panel (no CSS conflicts with host page)
  • File drag-and-drop for binary encryption
  • Auto-save and find control files
  • Clipboard auto-clear after 30 seconds
  • Manifest V3, Web Crypto API (all crypto runs locally)

Install

Load the extension/ directory as an unpacked extension in Chrome. Chrome Web Store submission is pending.

Full extension docs: deny.sh/extension


Telegram Bot NEW

Deniable encryption inside Telegram. @denyshbot

Commands

/start     - Get started
/encrypt   - Encrypt a message (interactive, multi-step)
/decrypt   - Decrypt a ciphertext
/protect   - Protect a seed phrase (BIP-39 validated)
/vault     - List saved control files
/help      - Show all commands
/cancel    - Cancel current operation

How it works

  • Multi-step conversation flows guide you through encryption
  • Password messages are auto-deleted from chat
  • Control files sent as .dat documents
  • Per-user SQLite vault for storing control files
  • DM-only for crypto operations (groups redirect to DM)
  • Reply to any message with /encrypt to encrypt it

Full bot docs: deny.sh/telegram


MCP Server NEW

Model Context Protocol server for AI agents. 11 tools: 4 run locally (zero knowledge, no API key), 7 use the API.

Local tools (no API key needed)

deny_local_encrypt      - Encrypt text locally
deny_local_decrypt      - Decrypt text locally
deny_local_create_decoy - Generate deniable control data locally
deny_local_shamir_split - Split a secret into shares locally

API tools (require API key)

deny_encrypt      - Encrypt via API
deny_decrypt      - Decrypt via API
deny_create_decoy - Create decoy via API
deny_vault_store  - Store to vault
deny_vault_list   - List vault items
deny_vault_get    - Get vault item
deny_usage        - Check API usage

Configuration

// Add to your MCP client config (e.g. claude_desktop_config.json)
{
  "mcpServers": {
    "deny-sh": {
      "command": "node",
      "args": ["path/to/deny-mcp-server.cjs"],
      "env": {
        "DENY_API_KEY": "dk_your_key_here"  // optional, only for API tools
      }
    }
  }
}

Full MCP docs: deny.sh/agents


1Password integration NEW

Push and pull encrypted control files to 1Password. Requires the op CLI to be installed and authenticated.

# Push control data to 1Password
deny-sh 1p push -l "Wallet backup" -c control.dat

# Pull control data from 1Password
deny-sh 1p pull -l "Wallet backup" -o control.dat

# List stored items
deny-sh 1p list

# Check connection status
deny-sh 1p status

Control data is stored as a Secure Note in your 1Password vault. The note contains base64-encoded control data and metadata (label, timestamp, ciphertext hash).


Bitwarden integration NEW

Push and pull encrypted control files to Bitwarden. Requires the bw CLI to be installed and authenticated.

# Push control data to Bitwarden
deny-sh bw push -l "Wallet backup" -c control.dat

# Pull control data from Bitwarden
deny-sh bw pull -l "Wallet backup" -o control.dat

# List stored items
deny-sh bw list

# Check connection status
deny-sh bw status

Handles BW_SESSION management automatically. Stored as Secure Note type 2 items.


Cloud backup NEW

Encrypted archive backups to Google Drive, Dropbox, S3, or local disk. Archives use scrypt + AES-256-CTR with the DENY_BACKUP_V1 format.

Backup to local disk17 lines
# Backup to local disk
deny-sh backup create -o ~/backups/

# Backup to Google Drive
deny-sh backup create --target gdrive

# Backup to Dropbox
deny-sh backup create --target dropbox

# Backup to S3
deny-sh backup create --target s3 --bucket my-bucket

# Restore from backup
deny-sh backup restore -i backup-2026-04-05.deny

# List backups
deny-sh backup list

Backups include your vault, control files, and .deny/ directory. The archive is encrypted with your passwords before upload, so cloud providers never see your data.


Pricing

Free Dev $99/mo Pro $199/mo Agents Infrastructure $999/mo
Monthly API calls 500 10,000 100,000 1,000,000
Encrypt / Decrypt / Deny
Control file vault 5 items 100 items 1,000 items 10,000 items
Dead man's switch 1 switch, 2 recipients 5 switches, 5 recipients 20 switches, 10 recipients 20 switches, 10 recipients
Steganography API 100 calls 10,000 calls 100,000 calls 1,000,000 calls
Audit log
Support GitHub Email Priority + SLA Priority + SLA
Annual pricing $950/yr (save 20%) $1,910/yr (save 20%) Contact sales

Lifetime Pro: one-time $499 for 100,000 calls/mo forever. Launch-week only, capped at 200 keys. See deny.sh/pricing.

Browser tools (encrypt, protect, stego, shamir, verify) are free forever with no limits. CLI local operations are free forever too.

To upgrade: deny.sh/pricing or call POST /api/billing/checkout with your API key.


Commercial licensing

The deny.sh SDK (TypeScript, Rust, Go, Python) is Apache License 2.0: free for commercial and proprietary embedding. The application layer (vault, dead-man's switch, MCP server orchestration) remains AGPL-3.0; a commercial licence is required only if you want to self-host that code in a proprietary product. See deny.sh/licensing.

Download the OpenAPI spec for Postman, code generation, or AI tooling.

Commercial application-layer licences start at $25,000/year, priced by deployment scale. Tell us what you're building and we'll scope the right licence.

hello@deny.sh for licensing enquiries. See Enterprise for dedicated infrastructure and SLA.


Errors

All errors return JSON with an error field and an appropriate HTTP status code:

{ "error": "Missing or invalid API key" }                              // 401
{ "error": "Monthly limit reached" }                                   // 429
{ "error": "Required: message, password1, password2" }                 // 400
{ "error": "Decryption failed. Check your passwords and control data." } // 400
{ "error": "plan must be \"dev\", \"pro\", \"dev-annual\", or \"pro-annual\"" }  // 400
{ "error": "ciphertext and controlData must be valid hex strings" }    // 400
{ "error": "Message too large (max 10MB)" }                            // 400
{ "error": "Password too long (max 1024 chars)" }                     // 400

Rate limiting: The API enforces per-key monthly limits based on your plan tier, plus burst rate limiting to prevent abuse. If you hit a rate limit, back off and retry after a few seconds.