API Reference
Base URL: https://voidnote.net
All /api/v1/ endpoints return JSON, support CORS, and accept
Authorization: Bearer vn_... for authenticated routes.
Machine-readable spec: /api/v1/openapi.json
Security model
The v1 note API is zero-knowledge. The server stores only ciphertext and the key stays in the URL fragment — plaintext never leaves your machine.
SHA-256(hex_decode(secret)), or for a password-protected note
PBKDF2-HMAC-SHA256(password, salt = hex_decode(secret), 600000 iterations, 32 bytes). https://voidnote.net/n/<tokenId>#<secret>. The #fragment is never sent to the server per HTTP spec. Authentication
Creating a note works without authentication, within the anonymous limits below.
With a vn_ API key from your
dashboard
(accounts are free), notes get the account limits and appear in your history.
Reading and peeking are unauthenticated. Creating a stream needs an account.
/api/v1/note/ auth optional
Create a note. You encrypt the content; the server generates a tokenId and
returns a base URL. Append #secret to form the full shareable link.
Without authentication, creation is rate-limited per IP address.
| Field | Type | Description |
|---|---|---|
| encrypted * | string | AES-256-GCM ciphertext (hex-encoded) |
| iv * | string | 12-byte GCM nonce (hex-encoded) |
| viewLimit | int | Views before destruction. 1–10 without an account, 1–100 with one. Default 1 |
| expiresIn | int | Lifetime in seconds. 60–604800 (7 days) without an account, 60–2592000 (30 days) with one. Default 3600 |
| kdf | string | "sha256" (default) or "pbkdf2-sha256-600k" for a password-protected note. Tells the reader how to derive the key |
{
"token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"url": "https://voidnote.net/n/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"expiresAt": "2025-01-01 12:00:00",
"viewLimit": 1,
"kdf": "sha256"
}
Append #<secret> to url — that's the shareable link.
The secret never touches the server.
/api/v1/note/:tokenId/consume/ no auth Fetch the encrypted blob and decrement the view count. Returns the ciphertext — decrypt locally. On the final view the note is permanently destroyed.
{
"token": "a1b2c3...",
"encrypted": "...", // hex — AES-256-GCM ciphertext
"iv": "...", // hex — 12-byte nonce
"algorithm": "AES-256-GCM",
"kdf": "sha256", // or "pbkdf2-sha256-600k" — see Security model
"viewsRemaining": 0,
"destroyed": true
} | Status | Meaning |
|---|---|
| 200 | Encrypted blob returned, view counted |
| 410 | Note not found, expired, or already destroyed |
| 429 | Rate limited (60 req/min unauthenticated) |
/api/v1/note/:tokenId/meta/ no auth · no view consumed Peek at metadata without consuming a view. Use this to check if a note still exists before revealing it.
{
"token": "a1b2c3...",
"exists": true,
"type": "secure",
"createdAt": "2025-01-01 10:00:00",
"expiresAt": "2025-01-01 11:00:00",
"viewLimit": 1,
"viewsRemaining": 1,
"kdf": "sha256"
} Vault
Encrypted file storage. Upload once, download once — the file is destroyed after the first download.
/api/v1/vault/:tokenId/meta Returns {"exists","fileName","sizeBytes","createdAt","expiresAt"} — no auth, no download.
/api/v1/vault/:tokenId/consume
Download encrypted binary. File is destroyed after this call.
IV is returned in X-Vault-IV header.
Decrypt: key = SHA-256(hex_decode(secret)).
Streams
Live encrypted channels over SSE. The server relays ciphertext only. The stream token in the URL path includes the key, so it reaches the server with each request; it is not stored. Streams auto-expire after their TTL and can be closed early at any time.
/api/stream auth required
Create a stream. Pass tokenId (first 32 chars of a client-generated 64-char hex token),
ttl (seconds: 3600, 21600, or 86400),
mode ("broadcast" or "chat"),
and optional title and password.
/api/stream/:token/write Send encrypted content to all connected listeners. Body: {"content": "..."}
/api/stream/:token/events SSE endpoint. Connect to receive real-time encrypted messages as text/event-stream.
/api/stream/:token/close Close and destroy the stream immediately. Owner only.
SDK examples
All SDKs handle encryption/decryption automatically. No crypto boilerplate needed.
Python
import voidnote
result = voidnote.create(
"my secret API key",
api_key="vn_...",
max_views=1,
expires_in=1, # hours
)
print(result.url) # https://voidnote.net/n/<token>#<secret> import voidnote
note = voidnote.read("https://voidnote.net/n/<token>#<secret>")
print(note.content)
print(f"{note.views_remaining} views remaining") import voidnote
meta = voidnote.peek("https://voidnote.net/n/<token>#<secret>")
print(meta.exists, meta.views_remaining, meta.expires_at) TypeScript / JavaScript
import { create } from "voidnote-sdk";
const result = await create("my secret", {
apiKey: "vn_...",
maxViews: 1,
expiresIn: 1,
});
console.log(result.url); // https://voidnote.net/n/<token>#<secret> import { read } from "voidnote-sdk";
const note = await read("https://voidnote.net/n/<token>#<secret>");
console.log(note.content); Go
result, err := voidnote.Create("my secret", voidnote.CreateOptions{
APIKey: "vn_...",
MaxViews: 1,
ExpiresIn: 1,
})
fmt.Println(result.URL) Java
import net.voidnote.*;
CreateResult result = VoidNote.create(
"my secret API key",
CreateOptions.of("vn_...")
.withMaxViews(1)
.withExpiresIn(1));
System.out.println(result.url()); ReadResult note = VoidNote.read("https://voidnote.net/n/TOKEN#SECRET");
System.out.println(note.content());
System.out.println(note.destroyed() ? "destroyed" : note.viewCount() + " views"); StreamHandle stream = VoidNote.createStream(
StreamOptions.of("vn_...").withTitle("Deploy log"));
System.out.println("share: " + stream.url);
stream.write("Build started...");
stream.write("All tests passed");
stream.close(); Swift
import VoidNote
let result = try await VoidNote.create(
"my secret API key",
apiKey: "vn_...",
maxViews: 1,
expiresIn: 1)
print(result.url) let note = try await VoidNote.read("https://voidnote.net/n/TOKEN#SECRET")
print(note.content)
print("views remaining: \(note.viewsRemaining)") let stream = try await VoidNote.createStream(
apiKey: "vn_...", title: "Deploy log")
print("share: \(stream.url)")
try await stream.write("Build started...")
try await stream.write("All tests passed")
try await stream.close() Rate limits
| Auth | Limit | Window |
|---|---|---|
| Create a note — authenticated (Bearer vn_...) | 300 · 5,000 | per minute · per day |
| Create a note — no account (per IP; IPv6 per /64) | 10 · 100 | per minute · per day |
| Consume — authenticated (Bearer vn_...) | 300 | per minute |
| Consume without auth, and meta (per IP) | 60 | per minute |