API
Any website or app can use Cachet’s API for free to create and open timed, encrypted messages. Messages made through the API work with the Cachet keyboard and this website, and the other way round.
| Base URL | https://www.jlkrkre.cn |
|---|---|
| Format | JSON, UTF-8. Every endpoint allows cross-origin calls (CORS). No sign-up, no API key. |
| Rate limits | Per IP per minute: 60 creates, 600 reads; /v1/open with a password counts as 10 reads. You may use a minute's allowance at once. Beyond that you get 429 with Retry-After (seconds). An overloaded server answers 503 {"error":"busy"}, also with Retry-After: just try again shortly. |
Two ways in
| Zero-knowledge (recommended) | Server-side encryption | |
|---|---|---|
| Endpoints | POST /v1/keysGET /v1/keys/{id}GET /v1/keys/{id}/status | POST /v1/sealPOST /v1/open |
| Where encryption happens | Your device or browser (with our SDK) | Our server |
| Can the server see the text? | No | Yes (in memory only, never stored or logged) |
| Good for | Websites, apps, anything privacy-sensitive | Back-end scripts, bots, quick integrations |
Option 1: the web SDK (zero-knowledge)
This site runs this very SDK: sdk/cachet.js (about 85 KB, unminified, ready to read). Put it on your own site and import it as an ES module:
import * as Cachet from './cachet.js';
// Seal: readable for 1 hour, optional access password and read-once
const { ciphertext, expiresAt } = await Cachet.seal('See you at eight', {
ttl: 3600, // seconds, 30 s to 30 days
password: '', // optional
burn: false, // true = opens only once
style: 'han', // 'han' | 'latin' | 'invisible'
server: 'https://www.jlkrkre.cn',
});
// Open: pass a whole chat message, the SDK finds the sealed text itself
try {
const { text, expiresAt, reads } = await Cachet.open(ciphertext, { password: '' });
} catch (e) {
// e.code: not_cachet | password_required | wrong_password | expired | consumed | not_found
}
// Look without opening (doesn't count as a read)
const info = Cachet.inspect(ciphertext); // { hasPassword, burn, ... } or null
Option 1: your own implementation
If you encrypt and decrypt yourself in another language, these are the only endpoints you need; the format is under Protocol.
POST /v1/keys: create a key
curl -X POST BASE/v1/keys -H 'Content-Type: application/json' \
-d '{"ttl": 3600, "maxReads": 0}'
| Field | Meaning |
|---|---|
ttl | Lifetime in seconds, 30 to 2592000 (30 days) |
maxReads | How many reads are allowed; 0 means no limit. 1 for read-once, 100 at most |
201 {"id": "Rk2x…", "key": "Q4m…", "expiresAt": 1791900000, "now": 1791896400}
id and key are unpadded base64url: id decodes to 12 bytes, key to 32.
GET /v1/keys/{id}: read a key
curl BASE/v1/keys/Rk2x…
200 {"key": "Q4m…", "expiresAt": 1791900000, "now": 1791896500, "reads": 1, "maxReads": 0}
410 {"error": "expired", "expiresAt": …} expired, key wiped
410 {"error": "consumed", "reads": 1, …} read-once, already read
404 {"error": "not_found"}
Every successful read is counted; the read that reaches maxReads wipes the key right after.
GET /v1/keys/{id}/status: check status
curl BASE/v1/keys/Rk2x…/status
200 {"expiresAt": 1791900000, "now": 1791896500, "maxReads": 1} still readable (reads omitted when 0)
410 {"error": "consumed", …} read-once, already read
410 {"error": "expired", …} / 404 as above
Like reading the key, but it returns no key and doesn’t count as a read. A sender can use it to see whether their read-once message has been opened.
Option 2: server-side encryption
POST /v1/seal: encrypt
curl -X POST BASE/v1/seal -H 'Content-Type: application/json' \
-d '{"text": "See you at eight", "ttl": 3600, "password": "", "burn": false, "style": "han"}'
201 {"ciphertext": "「…」", "id": "Rk2x…", "expiresAt": 1791900000, "now": 1791896400}
text is at most 10,000 characters; style is han (default, shortest), latin or invisible.
POST /v1/open: decrypt
curl -X POST BASE/v1/open -H 'Content-Type: application/json' \
-d '{"ciphertext": "From a friend: 「…」", "password": ""}'
200 {"text": "See you at eight", "expiresAt": 1791900000, "now": …, "reads": 1}
401 {"error": "password_required"}
403 {"error": "wrong_password"} a wrong password doesn't count as a read
410 {"error": "expired" | "consumed"}
422 {"error": "not_cachet" | "private_message"}
Limits and retries
When many people share one IP (an office, carrier NAT), a client can get a free pass for an allowance of its own (one IP tops out at 600 creates and 6000 reads a minute). A pass lasts 24 hours. The server doesn't store it and it isn't tied to any identity. Fetch one only after a 429.
GET /v1/pass/challenge → 200 {"challenge": "…", "bits": 18, "now": …}
Find a decimal number n such that SHA-256(challenge + ":" + n) starts with at least bits zero bits
POST /v1/pass {"challenge": "…", "solution": "n"} → 201 {"pass": "…", "expiresAt": …, "now": …}
Then send the header X-Cachet-Pass: …
Retrying a read-once message: send X-Cachet-Read-Token (a random value per open, up to 64 characters) when reading a key. If the reply gets lost you can read again with the same value for 30 seconds, without counting another read; anyone else still gets 410 consumed. Without this header the key is destroyed as it's read.
Protocol
The complete format, for zero-knowledge implementations in other languages. The Android, iOS, server and web implementations are checked byte for byte against the same test vectors.
envelope = 0x18 ‖ flags(1) ‖ keyId(12) ‖ AES-256-GCM(K, nonce = 12 × 0x00, aad = first 14 bytes, plaintext) flags: bit0 = access password, bit1 = read once K = HKDF-SHA256(ikm = key, salt = keyId, info = "cachet v2 public", 32) password: ikm = key ‖ PBKDF2-SHA256(UTF-8(password), salt = keyId, 100000 iterations, 32) plaintext = 1 format byte ‖ content; 0x00 UTF-8, 0x01 UTF-16BE, 0x02 raw DEFLATE(UTF-8)
The envelope bytes are then written as text:
- Hanzi:
「+ base-3755 digits +」, digits being the GB2312 level-1 characters in code order; a0x01byte is put in front of the envelope first. - Letters:
✉+ base-62 digits (0-9A-Za-z), also with a leading0x01. - Invisible:
🔏+ one Unicode variation selector per byte (U+FE00+b below 16, else U+E0100+b−16).