Production: https://timeseal.online
Development: http://localhost:3000
No authentication required. All endpoints are public.
- Seal Creation: 10 requests/minute per IP
- Seal Access: 20 requests/minute per IP
- Pulse Operations: 20 requests/minute per IP
Rate limit headers:
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Requests remaining in windowRetry-After: Seconds until rate limit resets (on 429 response)
POST /api/create-seal
Creates a new time-locked seal with encrypted content.
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
encryptedBlob |
Blob | Yes | Encrypted content (max 25MB) |
keyB |
string | Yes | Server-side encryption key (base64) |
iv |
string | Yes | Initialization vector (base64) |
unlockTime |
string | Yes | Unix timestamp (milliseconds) |
isDMS |
string | No | "true" for Dead Man's Switch |
pulseInterval |
string | No | Milliseconds between pulses (DMS only) |
pulseToken |
string | No | UUID for pulse authentication (DMS only) |
cf-turnstile-response |
string | Yes | Turnstile CAPTCHA token |
{
"success": true,
"publicUrl": "/v/abc123def456",
"pulseToken": "uuid-v4-token"
}- 400 Bad Request: Invalid input or validation failure
- 413 Payload Too Large: File exceeds 25MB
- 429 Too Many Requests: Rate limit exceeded
curl -X POST https://timeseal.online/api/create-seal \
-F "encryptedBlob=@encrypted.bin" \
-F "keyB=base64encodedkey" \
-F "iv=base64encodediv" \
-F "unlockTime=1735689600000" \
-F "isDMS=false" \
-F "cf-turnstile-response=token"GET /api/seal/{id}
Retrieves seal status and encrypted content. Returns Key B only if unlocked.
| Parameter | Type | Location | Description |
|---|---|---|---|
id |
string | Path | Seal ID (32 hex characters) |
{
"id": "abc123def456",
"isLocked": true,
"unlockTime": 1735689600000,
"timeRemaining": 86400000
}{
"id": "abc123def456",
"isLocked": false,
"unlockTime": 1735689600000,
"keyB": "base64encodedkey",
"iv": "base64encodediv",
"encryptedBlob": "base64encryptedcontent"
}- 404 Not Found: Seal does not exist
- 429 Too Many Requests: Rate limit exceeded
curl https://timeseal.online/api/seal/abc123def456POST /api/pulse
Resets the unlock timer for a Dead Man's Switch seal.
Content-Type: application/json
{
"pulseToken": "uuid-v4-token"
}{
"success": true,
"newUnlockTime": 1735776000000
}- 400 Bad Request: Invalid pulse token
- 404 Not Found: Seal not found or not a DMS
- 429 Too Many Requests: Rate limit exceeded
curl -X POST https://timeseal.online/api/pulse \
-H "Content-Type: application/json" \
-d '{"pulseToken":"uuid-v4-token"}'POST /api/pulse/status
Retrieves current status of a Dead Man's Switch seal.
Content-Type: application/json
{
"pulseToken": "uuid-v4-token"
}{
"timeRemaining": 86400000,
"pulseInterval": 604800000
}- 400 Bad Request: Invalid pulse token
- 404 Not Found: Seal not found
- 429 Too Many Requests: Rate limit exceeded
POST /api/burn
Permanently destroys a Dead Man's Switch seal (irreversible).
Content-Type: application/json
{
"pulseToken": "uuid-v4-token"
}{
"success": true,
"message": "Seal burned successfully"
}- 400 Bad Request: Invalid pulse token
- 404 Not Found: Seal not found or not a DMS
- 429 Too Many Requests: Rate limit exceeded
GET /api/audit/{id}
Retrieves immutable audit log for a seal.
| Parameter | Type | Location | Description |
|---|---|---|---|
id |
string | Path | Seal ID (32 hex characters) |
{
"sealId": "abc123def456",
"events": [
{
"timestamp": 1735689600000,
"eventType": "SEAL_CREATED",
"ip": "192.168.1.1",
"metadata": {
"isDMS": false,
"unlockTime": 1735776000000
}
},
{
"timestamp": 1735776000000,
"eventType": "SEAL_UNLOCKED",
"ip": "192.168.1.2",
"metadata": {}
}
]
}SEAL_CREATED: Seal was createdSEAL_UNLOCKED: Seal was successfully unlockedSEAL_ACCESS_DENIED: Attempted access while lockedPULSE_UPDATED: Dead Man's Switch pulse receivedSEAL_BURNED: Seal permanently destroyed
GET /api/health
Returns service health status.
{
"status": "healthy",
"timestamp": 1735689600000,
"version": "0.2.0"
}GET /api/metrics
Returns service metrics (Prometheus format).
# HELP timeseal_seals_created_total Total number of seals created
# TYPE timeseal_seals_created_total counter
timeseal_seals_created_total 1234
# HELP timeseal_seals_unlocked_total Total number of seals unlocked
# TYPE timeseal_seals_unlocked_total counter
timeseal_seals_unlocked_total 567
# HELP timeseal_pulses_received_total Total number of pulses received
# TYPE timeseal_pulses_received_total counter
timeseal_pulses_received_total 89
All error responses follow this structure:
{
"error": "Human-readable error message"
}Common HTTP status codes:
400: Bad Request (validation error)404: Not Found (seal doesn't exist)413: Payload Too Large (file > 25MB)429: Too Many Requests (rate limit)500: Internal Server Error
-
Generate Keys:
const keyA = crypto.getRandomValues(new Uint8Array(32)); const keyB = crypto.getRandomValues(new Uint8Array(32)); const iv = crypto.getRandomValues(new Uint8Array(12));
-
Encrypt Content:
const key = await crypto.subtle.importKey( 'raw', combineKeys(keyA, keyB), { name: 'AES-GCM' }, false, ['encrypt'] ); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, content );
-
Create Seal:
- Send
encryptedBlob,keyB,ivto server - Keep
keyAin URL hash (never sent to server)
- Send
-
Decrypt Content (after unlock):
- Get
keyBfrom server (only when unlocked) - Combine
keyA(from URL) +keyB(from server) - Decrypt using combined key
- Get
- Server validates unlock time using
Date.now()(server-side) - Client clock manipulation has no effect
- Key B is withheld until
serverTime >= unlockTime
- Key A: Stored in URL hash, never sent to server
- Key B: Encrypted and stored in database
- Both keys required for decryption
- Per-IP address tracking
- 429 status with
Retry-Afterheader - Prevents brute-force attacks
- Turnstile CAPTCHA required for seal creation
- Prevents automated abuse
- Pulse tokens include nonces
- Nonce validation prevents replay attacks
See openapi.yaml for the complete OpenAPI 3.0 specification.
import { encryptData } from '@/lib/crypto';
// Create seal
const encrypted = await encryptData('secret message');
const formData = new FormData();
formData.append('encryptedBlob', new Blob([encrypted.encryptedBlob]));
formData.append('keyB', encrypted.keyB);
formData.append('iv', encrypted.iv);
formData.append('unlockTime', Date.now() + 86400000);
formData.append('cf-turnstile-response', turnstileToken);
const response = await fetch('/api/create-seal', {
method: 'POST',
body: formData
});
const { publicUrl } = await response.json();
const vaultLink = `${window.location.origin}${publicUrl}#${encrypted.keyA}`;import requests
import time
# Get seal status
seal_id = "abc123def456"
response = requests.get(f"https://timeseal.online/api/seal/{seal_id}")
data = response.json()
if data['isLocked']:
print(f"Locked. Time remaining: {data['timeRemaining']}ms")
else:
print(f"Unlocked! Key B: {data['keyB']}")# Send pulse
curl -X POST https://timeseal.online/api/pulse \
-H "Content-Type: application/json" \
-d '{"pulseToken":"your-uuid-token"}'- Documentation: GitHub
- Issues: GitHub Issues
- Security: See SECURITY.md