Paste

NULL paste at paste.nullnet.sh is a zero-knowledge pastebin for text and files. Your client encrypts the paste before upload. The server stores ciphertext it cannot read, and the key travels only in the link you share. The API base is https://paste.nullnet.sh/api.

The API calls a stored paste a drop. The paste service has no accounts of its own; anyone can create a drop. A NULL Premium pass raises the limits and unlocks extra features.

Zero-knowledge model

"Zero-knowledge" here means the server learns nothing about the content. It does not mean zero-knowledge proofs.

  1. The client builds a plaintext (a small JSON manifest plus the text or file bytes), generates a fresh random 32-byte key, and encrypts the plaintext with AES-256-GCM into an envelope.
  2. The client uploads the envelope. The server checks only its version byte and length, stores it, and returns a drop id and a manage token.
  3. The client builds the share link with the key in the URL fragment (the part after #). Browsers never send the fragment to a server.
  4. The recipient's client fetches the envelope by id and decrypts it locally with the key from the fragment.

The server never receives the key, the plaintext, filenames, media types or the number of files. It does see the drop id, the envelope length (rounded up by padding), the expiry and view options, request timing and the connecting IP address. For a password-protected drop it also sees that a password is used and the Argon2id parameters.

Anyone holding the full link can decrypt the drop. Treat the link as the secret. The byte-level format is on the envelope format page.

Share link    https://paste.nullnet.sh/p/<id>#k=<key>
              https://paste.nullnet.sh/p/<id>#k=<key>&p=1      password-protected
Manage link   https://paste.nullnet.sh/m/<id>#t=<manage_token>
  • <id> is 22 base64url characters (128 random bits).
  • <key> is the 32-byte key as unpadded base64url, always 43 characters.
  • p=1 tells the reader to ask for a password before it opens the drop. It is only a hint; the envelope itself says whether a password is needed.
  • The manage link carries the 43-character manage token, which deletes or extends the drop. The web app may also append &k=<key> to the manage link so you can copy the share link again later.

Never put the key, the password or the manage token into a log or an error report, and never send the key to the API. The password is never part of any link.

Drop options

Each drop has an expiry and, optionally, a view limit. The expiry is chosen at creation from a fixed list of presets.

  • TTL presets. Free: 10 minutes, 1 hour, 24 hours (600, 3600, 86400 seconds). Premium adds 7 days and 30 days (604800, 2592000). Reading a drop never extends it.
  • Burn after reading. burn=true allows exactly one open. The drop is deleted as soon as the server reads it for that open.
  • View limit. views=n allows n opens (free: up to 10, premium: up to 100), then the drop is deleted. Without views or burn, a drop can be opened any number of times until it expires.
  • Password. The content key is derived from both the link key and a password, so neither works alone. The password never leaves the client. See version 2.
  • Extend. With the manage token you can set a new expiry from the presets, up to 24 hours after creation for anonymous drops and up to 30 days for premium drops. See extend.

A view is consumed when the server reads the drop, before delivery is confirmed. If the connection drops during an open, a burn drop can be lost. A drop that is gone, expired, consumed or never existed always returns the same 404.

Retention

Drops are kept in memory only, not on durable storage. If the service restarts its store, every drop is lost, whatever its expiry. The expiry you choose is a maximum, not a promise of durability. Do not use paste as the only copy of anything.

Large premium envelopes (over 1 MiB + 16 KiB) are written to the server's disk instead. They are not backed up and are deleted when the drop expires, is consumed or is deleted. Deletion removes the file from the service; it is not a forensic erase of the disk. The content is ciphertext either way, but for secrets prefer burn after reading.

Free limits

Ask the server what you may do instead of hard-coding limits. GET /api/limits returns the caller's current limits and needs no authentication. This is the live answer for an anonymous caller:

Request

curl https://paste.nullnet.sh/api/limits

Response

{
  "tier": "free",
  "max_envelope_bytes": 1064960,
  "ttl_presets_secs": [600, 3600, 86400],
  "max_views": 10,
  "max_files": 1,
  "uploads_per_min": 10,
  "reads_per_min": 60,
  "features": [],
  "quota": null,
  "pass_expires_at": null
}
  • Size. The envelope may be up to 1 MiB + 16 KiB (1,064,960 bytes), so the plaintext may be up to 1 MiB.
  • Files. One entry per drop. Bundles of several files are a premium feature.
  • Rate limits per client IP: 10 uploads per minute; 60 reads per minute (meta, open, delete, extend and limits combined); 5 abuse reports per minute. Over a limit you get 429 rate_limited with a Retry-After header in seconds.
  • Upload time. The body must arrive within 15 seconds.

Field meanings are in the limits reference. Premium values are on the Premium page.

Errors

Errors are JSON with a stable code and a human-readable message. Match on code; the message may change.

Error

{ "error": { "code": "not_found", "message": "drop not found" } }
StatuscodeWhen
400invalid_queryThe query string does not parse (missing ttl, not a number).
400invalid_optionsttl is not one of your presets, views is out of range, burn with views other than 1, or an extension past the maximum.
400invalid_envelopeUnknown version byte, shorter than the version's minimum (29 bytes, or 87 for version 2), or a version 2 header with an unknown KDF or Argon2id parameters out of bounds.
400invalid_jsonA report body that is not JSON, not application/json, over 8 KiB, missing id or reason, or with unknown members.
400invalid_reportA report with a malformed id, an unknown reason, or a bad note.
400body_readThe client stopped sending the body.
400invalid_headerIf-Match is not one quoted version, or X-Zkbin-Alert-To is not a plain mailbox on an allowed domain.
401missing_tokenDelete or extend without a well-formed Authorization: Bearer manage token.
403wrong_tokenDelete or extend with a token that belongs to another drop.
403tier_requiredA premium feature (/api/vault, X-Zkbin-Alert-To) without a pass that enables it.
404not_foundMissing, expired, consumed, or not a valid id. These cases are indistinguishable on purpose.
408body_timeoutThe upload did not arrive within 15 seconds (10 minutes for premium uploads over 1 MiB + 16 KiB).
412vault_conflictPUT /api/vault whose If-Match is not the stored version.
413too_largeThe body is larger than your max_envelope_bytes (or the vault cap).
429quota_exceededA premium upload over the account's live drops or live storage. No Retry-After; it frees up as drops end.
429rate_limitedToo many requests. Retry-After gives the seconds to wait.
503storage_fullThe service has no room for new drops. Existing drops are kept.
503reports_fullToo many drops already have open reports. Write to support@nullnet.sh.
503unavailableStorage is unreachable or too slow. Retry later.
500internalA server bug. No detail is returned.

A request that runs past the server's time budget is answered with 503.

Every response carries Cache-Control: no-store, X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer.

Health

RequestResponse
GET https://paste.nullnet.sh/healthz200 with body ok while the service runs.
GET https://paste.nullnet.sh/readyz200 with body ready when storage answers, 503 otherwise.

Abuse

Anyone holding a link can report a drop with POST /api/reports, or by mail to support@nullnet.sh. Never include the #k= part of the link in a report.