Hakanai provides a RESTful API for programmatic access to secret sharing functionality.
📚 For interactive API documentation with examples, visit /docs on your running server.
The interactive docs are automatically generated from the OpenAPI specification and are always up-to-date with the current API implementation.
All API endpoints are relative to your server's base URL:
https://your-hakanai-server.com/api/v1
Authentication is optional but recommended for production deployments.
Authorization: Bearer {your-token}- Admin tokens: Created with
--enable-admin-tokenserver flag - User tokens: Created via admin API or auto-generated default token
- Anonymous access: Available when
--allow-anonymousis enabled
Create a new secret with optional access restrictions.
POST /api/v1/secret
Content-Type: application/json
Authorization: Bearer {token} # Optional if anonymous access enabled
{
"data": "base64-encoded-secret-data",
"expires_in": 3600, // seconds (optional, default: 86400)
"restrictions": { // optional
"allowed_ips": ["192.168.1.0/24", "10.0.0.1", "2001:db8::/32"],
"allowed_countries": ["US", "DE", "CA"],
"allowed_asns": [13335, 15169, 202739],
"passphrase_hash": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
}
}- data (string, required): Base64-encoded secret data
- expires_in (integer, optional): TTL in seconds (default: 86400, max: server configured)
- restrictions (object, optional): Access control restrictions
- allowed_ips (array[string], optional): IP addresses and CIDR ranges
- allowed_countries (array[string], optional): ISO 3166-1 alpha-2 country codes
- allowed_asns (array[integer], optional): Autonomous System Numbers
- passphrase_hash (string, optional): SHA-256 hash of required passphrase
Success (201 Created):
{
"id": "550e8400-e29b-41d4-a716-446655440000"
}Error Responses:
- 400 Bad Request: Invalid request body or malformed data
- 401 Unauthorized: Invalid or missing token (when authentication required)
- 413 Payload Too Large: Secret data exceeds size limits
- 422 Unprocessable Entity: Invalid restrictions format
# Create simple secret
curl -X POST https://hakanai.example.com/api/v1/secret \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token" \
-d '{
"data": "bXkgc2VjcmV0IGRhdGE=",
"expires_in": 1800
}'
# Create secret with IP restrictions
curl -X POST https://hakanai.example.com/api/v1/secret \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token" \
-d '{
"data": "cmVzdHJpY3RlZCBzZWNyZXQ=",
"expires_in": 3600,
"restrictions": {
"allowed_ips": ["192.168.1.0/24", "10.0.0.1"],
"allowed_countries": ["US", "DE"]
}
}'
# Create passphrase-protected secret
curl -X POST https://hakanai.example.com/api/v1/secret \
-H "Content-Type: application/json" \
-d '{
"data": "cGFzc3dvcmQgcHJvdGVjdGVk",
"restrictions": {
"passphrase_hash": "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"
}
}'Retrieve a secret by its ID. One-time access only - the secret is permanently deleted after retrieval.
GET /api/v1/secret/550e8400-e29b-41d4-a716-446655440000
X-Secret-Passphrase: sha256-hash-of-passphrase # Required for passphrase-protected secretsSuccess (200 OK):
Content-Type: text/plain
decrypted secret data here
Error Responses:
- 401 Unauthorized: Missing or incorrect passphrase
- 403 Forbidden: Access denied due to IP/country/ASN restrictions
- 404 Not Found: Secret doesn't exist or has expired
- 410 Gone: Secret was already accessed by someone else
- 501 Not Implemented: Geo-restrictions used but server not configured
# Simple retrieval
curl https://hakanai.example.com/api/v1/secret/550e8400-e29b-41d4-a716-446655440000
# With passphrase
curl https://hakanai.example.com/api/v1/secret/550e8400-e29b-41d4-a716-446655440000 \
-H "X-Secret-Passphrase: 5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"
# Save to file
curl https://hakanai.example.com/api/v1/secret/550e8400-e29b-41d4-a716-446655440000 \
-o secret.txtCreate user authentication tokens. Requires admin authentication and trusted IP access.
POST /api/v1/admin/tokens
Content-Type: application/json
Authorization: Bearer {admin-token}
{
"upload_size_limit": 5242880, // bytes (optional)
"ttl_seconds": 2592000 // seconds (optional, default: 30 days)
}Success (201 Created):
{
"token": "generated-user-token-string"
}Error Responses:
- 401 Unauthorized: Invalid or missing admin token
- 403 Forbidden: Request not from trusted IP range
- 400 Bad Request: Invalid request body
# Create token with custom limits
curl -X POST https://hakanai.example.com/api/v1/admin/tokens \
-H "Content-Type: application/json" \
-H "Authorization: Bearer admin-token" \
-d '{
"upload_size_limit": 1048576,
"ttl_seconds": 604800
}'Returns 200 OK when the server is ready to accept requests.
curl https://hakanai.example.com/readyReturns 200 OK when the server and all dependencies (Redis) are healthy.
curl https://hakanai.example.com/healthyAlternative endpoint for secret access with dual behavior:
- CLI/API clients: Returns raw secret data
- Web browsers: Returns HTML interface for decryption
# CLI access (returns raw data)
curl https://hakanai.example.com/s/550e8400-e29b-41d4-a716-446655440000
# Browser access (returns HTML interface)
# Visit: https://hakanai.example.com/s/550e8400-e29b-41d4-a716-446655440000All endpoints return consistent error responses:
{
"error": "Human readable error message",
"details": "Additional context when available"
}- 200 OK: Successful secret retrieval
- 201 Created: Successful secret creation or token creation
- 400 Bad Request: Invalid request format or parameters
- 401 Unauthorized: Missing or invalid authentication
- 403 Forbidden: Access denied (IP/country/ASN restrictions)
- 404 Not Found: Secret not found or expired
- 410 Gone: Secret already accessed (one-time use)
- 413 Payload Too Large: Secret exceeds size limits
- 422 Unprocessable Entity: Invalid data format
- 501 Not Implemented: Feature requires server configuration
Secret data is serialized using MessagePack before encryption. The payload structure contains the raw bytes and an optional filename:
import msgpack
# Encode text secret
secret_text = "my secret message"
payload = [secret_text.encode('utf-8'), None] # [data, filename]
encoded = msgpack.packb(payload)
# Encode binary file with filename
with open('document.pdf', 'rb') as f:
binary_data = f.read()
payload = [binary_data, "document.pdf"] # [data, filename]
encoded = msgpack.packb(payload)// Using msgpack-lite in JavaScript
import msgpack from "msgpack-lite";
// Encode text secret
const secretText = "my secret message";
const payload = [new TextEncoder().encode(secretText), null];
const encoded = msgpack.encode(payload);
// Encode binary file with filename
const payload = [fileBytes, "document.pdf"];
const encoded = msgpack.encode(payload);The MessagePack-encoded payload is then encrypted with AES-256-GCM before being base64-encoded for HTTP transport.
Passphrases must be SHA-256 hashed before sending:
import hashlib
passphrase = "my secret passphrase"
hash_object = hashlib.sha256(passphrase.encode('utf-8'))
passphrase_hash = hash_object.hexdigest()# Using command line
echo -n "my secret passphrase" | sha256sum | cut -d' ' -f1- IPv4:
192.168.1.100 - IPv4 CIDR:
192.168.1.0/24 - IPv6:
2001:db8::1 - IPv6 CIDR:
2001:db8::/32
Use ISO 3166-1 alpha-2 country codes (uppercase):
US(United States)DE(Germany)CA(Canada)GB(United Kingdom)FR(France)JP(Japan)
Autonomous System Numbers as 32-bit unsigned integers:
13335(Cloudflare)15169(Google)16509(Amazon)32934(Facebook)
Rate limiting should be implemented at the reverse proxy level (nginx, Caddy, etc.). The application does not enforce rate limits directly.
Cross-Origin Resource Sharing (CORS) is restrictive by default. Configure allowed origins with:
# Server flag
--cors-allowed-origins "https://trusted-domain.com,https://another-domain.com"
# Environment variable
export HAKANAI_CORS_ALLOWED_ORIGINS="https://trusted-domain.com,https://another-domain.com"When implementing a Hakanai client in any language, ensure you:
- Base64 encode all secret data before sending
- SHA-256 hash passphrases before sending (never send plaintext)
- Include Bearer token in Authorization header if authenticated
- Handle HTTP status codes appropriately (401, 403, 404, 410)
import requests
import base64
import hashlib
# Create secret
def create_secret(base_url, data, token=None):
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
payload = {
'data': base64.b64encode(data.encode()).decode(),
'expires_in': 3600
}
response = requests.post(f'{base_url}/api/v1/secret',
json=payload, headers=headers)
return response.json()['id']
# Retrieve secret
def get_secret(base_url, secret_id, passphrase=None):
headers = {}
if passphrase:
headers['X-Secret-Passphrase'] = hashlib.sha256(
passphrase.encode()).hexdigest()
response = requests.get(f'{base_url}/api/v1/secret/{secret_id}',
headers=headers)
return response.text// Create secret
async function createSecret(baseUrl, data, token) {
const response = await fetch(`${baseUrl}/api/v1/secret`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : undefined,
},
body: JSON.stringify({
data: btoa(data),
expires_in: 3600,
}),
});
return (await response.json()).id;
}
// Retrieve secret
async function getSecret(baseUrl, secretId, passphrase) {
const headers = {};
if (passphrase) {
const encoder = new TextEncoder();
const data = encoder.encode(passphrase);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
headers["X-Secret-Passphrase"] = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}
const response = await fetch(`${baseUrl}/api/v1/secret/${secretId}`, { headers });
return await response.text();
}- Always use HTTPS in production
- Implement proper authentication with tokens
- Apply IP restrictions for sensitive secrets
- Use short TTLs for temporary data
- Hash passphrases before sending
- Validate all inputs on the client side
- Handle errors gracefully without exposing internal details
- Log API access for audit trails
401 Unauthorized:
- Check token is valid and not expired
- Ensure
Authorization: Bearer {token}header format - Verify server has authentication enabled
403 Forbidden:
- Check IP address against allowed ranges
- Verify country/ASN restrictions match client location
- Ensure admin API calls come from trusted IP ranges
413 Payload Too Large:
- Check secret size against server limits
- Account for base64 encoding overhead (~33% increase)
- Consider splitting large files
501 Not Implemented:
- Geo-restrictions require server configuration
- Set
--country-headeror--asn-headerflags - Configure reverse proxy to provide location headers
For more troubleshooting, see DEPLOYMENT.md.