Skip to main content

NullPaste api

Paste from curl

No account, no API key. Send raw text, get a link, a raw URL, and a one-time delete token back. Pastes created over the API get the same expiry, burn, and password options as the web editor — and the same 1 MB limit and per-IP rate limits.

Create a paste

curl --data-binary @file.txt \
  -H "Content-Type: text/plain" \
  https://nullpaste.org/api/v1/pastes

The request body is the paste text itself (Content-Type: text/plain — JSON bodies are rejected). A successful create returns 201:

{
  "id": "AbC123xYz012",
  "url": "https://nullpaste.org/AbC123xYz012",
  "raw_url": "https://nullpaste.org/raw/AbC123xYz012",
  "delete_token": "9f2kQ7wLpX4mR8sT2v",
  "expires_at": "2026-08-31T06:00:00.000Z",
  "burn": false,
  "encrypted": false
}

Save delete_token — it is shown only in this response, stored only as a hash, and is the only way to delete the paste before it expires.

HeaderValuesNotes
NP-Expiry10m | 1h | 1d | 1w | neverDefault: 1d
NP-Languageauto | <language id>Default: auto-detect
NP-Burn0 | 1Burn after first confirmed read
NP-Password<password>Argon2id access control (not E2E)
curl --data-binary @secrets.env \
  -H "Content-Type: text/plain" \
  -H "NP-Expiry: 10m" \
  -H "NP-Burn: 1" \
  -H "NP-Password: hunter2" \
  https://nullpaste.org/api/v1/pastes

Fetch a paste

GET /raw/<id> returns the paste as text/plain. GET /api/v1/pastes/<id> returns JSON with content plus metadata. Both follow the same rules:

  • Plaintext only: an end-to-end encrypted paste returns 409 from /raw — the server holds only ciphertext and never presents it as source code.
  • Password-protected pastes need NP-Password: <password> (or Authorization: Bearer <password>), otherwise 401.
  • Burn-after-read pastes need NP-Confirm-Burn: 1, otherwise 409 — reading one destroys it, so a stray curl can never consume it by accident. A consumed paste returns 410 with {"error":"burned"}.
# plain paste
curl https://nullpaste.org/raw/AbC123xYz012

# password-protected paste
curl -H "NP-Password: hunter2" https://nullpaste.org/raw/AbC123xYz012

# burn-after-read paste — reading destroys it, so confirmation is explicit
curl -H "NP-Confirm-Burn: 1" https://nullpaste.org/raw/AbC123xYz012

Delete a paste

curl -X DELETE \
  -H "NP-Delete-Token: 9f2kQ7wLpX4mR8sT2v" \
  https://nullpaste.org/api/v1/pastes/AbC123xYz012

Returns 200 on success. The token is single-use by construction: deletion removes the paste and its token hash, so it cannot be replayed.

End-to-end encryption from your own client

API-created pastes are plaintext on the server unless your client encrypts them first. To make a paste the server cannot read, encrypt to the same versioned format the web editor uses — AES-256-GCM, fresh 12-byte IV, enc1:<b64url iv>:<b64url ciphertext> — and put the key in the URL fragment yourself. The fragment never reaches the server. Full spec on the security page.

JavaScript (Node 20+)

// Node 20+: E2E-encrypt before upload (same scheme the browser uses)
const { webcrypto: crypto } = require("node:crypto");

const b64url = (buf) => Buffer.from(buf).toString("base64url");

async function encryptAndPaste(plaintext) {
  const key = await crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 }, true, ["encrypt"]);
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ct = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv }, key, new TextEncoder().encode(plaintext));
  const body = `enc1:${b64url(iv)}:${b64url(ct)}`;

  const res = await fetch("https://nullpaste.org/api/v1/pastes", {
    method: "POST",
    headers: { "Content-Type": "text/plain" },
    body,
  });
  const paste = await res.json();
  const keyB64 = b64url(await crypto.subtle.exportKey("raw", key));
  return `${paste.url}#key=${keyB64}`; // full share link — key never uploaded
}

Python

# pip install cryptography requests
import base64, os, requests
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()

def encrypt_and_paste(plaintext: str) -> str:
    key = AESGCM.generate_key(bit_length=256)
    iv = os.urandom(12)
    ct = AESGCM(key).encrypt(iv, plaintext.encode(), None)  # tag appended
    body = f"enc1:{b64url(iv)}:{b64url(ct)}"
    r = requests.post(
        "https://nullpaste.org/api/v1/pastes",
        data=body, headers={"Content-Type": "text/plain"})
    paste = r.json()
    return f"{paste['url']}#key={b64url(key)}"  # key stays local

npaste shell function

# Add to ~/.bashrc or ~/.zshrc
npaste() {
  curl -s --data-binary @"${1:--}" \
    -H "Content-Type: text/plain" \
    https://nullpaste.org/api/v1/pastes
}

# Usage:
#   npaste file.txt          — paste a file
#   some-command | npaste    — paste from stdin

Rate limits & abuse

Creation, reads, and deletes are rate-limited per client. The identifier is an HMAC-SHA256 hash derived from your IP — the raw IP is never stored or logged. Exceeding a limit returns 429 with a retry-after header. Report abusive pastes via the Report button on any paste page or the contact page.