API
Base URL https://timelock.dev. No authentication. CORS is open (Access-Control-Allow-Origin: *), so you can call it from any site.
POST /api/encrypt
Encrypts a message so it can only be decrypted at or after a drand quicknet round. Give exactly one of duration, unlockAt or round.
curl -s https://timelock.dev/api/encrypt \
-H 'Content-Type: application/json' \
-d '{"plaintext": "hello, future", "duration": "5m"}'Request body
| Field | Type | Description |
|---|---|---|
plaintext | string, required | The message. Up to 64 KiB after decoding. |
encoding | "utf8" | "base64" | How plaintext is encoded. Default "utf8". Use base64 for binary data. |
duration | number | string | Seconds from now (300) or a duration string: "90s", "15m", "2h30m", "3 days 6 hours". Units: s, m, h, d, w, y (365 days). |
unlockAt | number | string | Unix seconds, or ISO 8601 with a time zone: "2030-01-01T00:00:00Z", "2030-01-01T09:00:00+09:00". Must be in the future. |
round | integer | An exact quicknet round. Past rounds are allowed; the result is decryptable immediately. |
Times are rounded up to the next round (one every 3 seconds), so unlockAt in the response can be up to 3 s later than requested. The maximum is 100 years ahead.
Response 200
{
"ciphertext": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2Nr…\n-----END AGE ENCRYPTED FILE-----\n",
"round": 33275913,
"unlockAt": "2026-10-22T01:05:03.000Z",
"unlockAtUnix": 1792631103,
"chainHash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
"network": "quicknet"
}More examples
# unlock at a specific moment
curl -s https://timelock.dev/api/encrypt \
-d '{"plaintext": "Happy new year!", "unlockAt": "2030-01-01T00:00:00Z"}'
# binary data, ciphertext saved to a file (needs jq)
curl -s https://timelock.dev/api/encrypt \
-d "{\"plaintext\": \"$(base64 -w0 photo.jpg)\", \"encoding\": \"base64\", \"duration\": \"30d\"}" \
| jq -r .ciphertext > photo.jpg.agePOST /api/decrypt
Decrypts a ciphertext once its round has been published. Accepts anything produced by this site, the API, dee, tlock or tlock-js for quicknet (armored form).
curl -s https://timelock.dev/api/decrypt \
-H 'Content-Type: application/json' \
-d "$(jq -Rs '{ciphertext: .}' message.age)"Request body
| Field | Type | Description |
|---|---|---|
ciphertext | string, required | Armored age text, -----BEGIN AGE ENCRYPTED FILE----- … -----END AGE ENCRYPTED FILE-----. |
encoding | "base64" | Optional. Force base64 output even when the plaintext is valid UTF-8. |
Response 200
{
"plaintext": "hello, future",
"encoding": "utf8",
"round": 33275913,
"unlockAt": "2026-10-22T01:05:03.000Z",
"unlockAtUnix": 1792631103
}Plaintext that isn't valid UTF-8 comes back as base64 with "encoding": "base64".
Too early: 425
{
"error": {
"code": "too_early",
"message": "Too early: this message unlocks at drand round 33275913 (2026-10-22T01:05:03.000Z).",
"round": 33275913,
"unlockAt": "2026-10-22T01:05:03.000Z",
"unlockAtUnix": 1792631103
}
}GET /api/events
The public activity feed, newest first. It contains no message content, ciphertext or IP addresses.
curl -s 'https://timelock.dev/api/events?limit=2'| Query | Description |
|---|---|
limit | 1 to 100, default 20. |
before | Return events with an id lower than this (pagination: pass the previous nextBefore). |
{
"events": [
{
"id": 42,
"type": "decrypt",
"source": "api",
"createdAt": "2026-10-22T01:06:10.512Z",
"round": 33275913,
"unlockAt": "2026-10-22T01:05:03.000Z"
},
{
"id": 41,
"type": "encrypt",
"source": "web",
"createdAt": "2026-10-22T01:00:01.204Z",
"round": 33275913,
"unlockAt": "2026-10-22T01:05:03.000Z"
}
],
"nextBefore": 41
}source is "web" (browser-side, reported by the website after the fact) or "api" (recorded by the server). nextBefore is null on the last page. Responses may be cached for 5 seconds. POST /api/events is used by the website itself and only accepts same-origin requests.
Original endpoints
The endpoints of the original Rust worker still work unchanged for existing scripts: raw bytes in, raw bytes out, no JSON. Uploads can be up to 5 MiB.
POST /encrypt/:round
Body: the raw plaintext (any bytes). Response: armored age ciphertext (Content-Type: application/octet-stream). Look up the round for a time with /api/encrypt or dee.
curl -s -X POST --data-binary @original.png https://timelock.dev/encrypt/33275913 > encrypted.agePOST /decrypt
Body: armored or binary age ciphertext. Response: the raw plaintext bytes.
curl -s -X POST --data-binary @encrypted.age https://timelock.dev/decrypt > decrypted.pngErrors on these endpoints are plain text with the HTTP status codes listed below (for example 425 when it's too early).
Errors
JSON endpoints return {"error": {"code": "…", "message": "…"}} with extra fields where noted.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json | Body isn't a JSON object. |
| 400 | invalid_request | A field is missing or invalid; `message` says which. |
| 403 | forbidden | Cross-origin call to an endpoint reserved for the website. |
| 405 | method_not_allowed | Wrong HTTP method. |
| 413 | payload_too_large | Body or plaintext over the limit; includes `limitBytes`. |
| 422 | invalid_ciphertext | Not a tlock age ciphertext, or it's corrupted. |
| 422 | unsupported_chain | Ciphertext was made for a drand chain other than quicknet. |
| 425 | too_early | Unlock round not reached yet; includes `round`, `unlockAt`, `unlockAtUnix`. |
| 500 | internal_error | Unexpected server error. |
| 502 | drand_unavailable | No drand relay answered. Retry with backoff. |
| 503 | feed_unavailable | The activity feed database isn't configured. |
Limits
- JSON plaintext: 64 KiB after decoding; JSON request body: 192 KiB.
- Original endpoints: 5 MiB per request. For larger files, encrypt locally with
dee; there's no size limit there. - Unlock time: up to 100 years ahead, at 3-second granularity.
- Network: drand quicknet only, chain hash
52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971. - Cloudflare may rate-limit abusive traffic. Please back off on
502.