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"
}
| Parameter | Type | Description |
|---|---|---|
| message required | string | Plaintext to encrypt. Max 10MB. |
| password1 required | string | Primary password. Max 1024 chars. |
| password2 required | string | Secondary password. Max 1024 chars. |
| controlDataHex | string | Optional 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"
}
| Parameter | Type | Description |
|---|---|---|
| ciphertext required | string | Hex-encoded ciphertext. |
| controlData required | string | Hex-encoded control data. |
| password1 required | string | Primary password. |
| password2 required | string | Secondary 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
}| Parameter | Type | Description |
|---|---|---|
| ciphertext required | string | Hex-encoded ciphertext from a previous encrypt call. |
| password1 required | string | Primary password (same as used for encryption). |
| password2 required | string | Secondary password (same as used for encryption). |
| fakeMessage required | string | The 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"
}
| Parameter | Type | Description |
|---|---|---|
| size required | number | Byte 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
.datdocuments - 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 listBackups 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 | 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.