API & CLI
Everything the website does is available over plain HTTP. No keys, no accounts. Responses are JSON unless the client looks like a terminal, in which case you get the URL back as text.
Install the CLI
Paste without opening the site. The Node version supports end-to-end encryption; the shell version needs only curl.
# Node 20+ (npm) — full featured, incl. -E for encryption
npm install -g @xditya/pastr
# No Node? POSIX shell + curl, with this host preconfigured
curl -fsSL https://pastr.xditya.me/install.sh | shls -la | pastr # stdin
pastr main.go --expires 1d # files (language from the extension)
pastr clip -E -c # clipboard → encrypted paste, link copied back
pastr text "hello there" -b # literal text, burn after read
pastr get https://pastr.xditya.me/AbCd1234#key # print (and decrypt) a paste
pastr ls # pastes made from this machine
pastr rm AbCd1234 # delete with the locally stored edit token
pastr token AbCd1234 # show that token, for the site's Edit/Delete promptPlain curl
# Pipe anything in and get a link back
cat main.go | curl --data-binary @- -H 'Content-Type: text/plain' 'https://pastr.xditya.me/api/v1/pastes?name=main.go'
# Options travel in the query string for raw bodies (burn accepts true/false, or just ?burn)
curl --data-binary @notes.md -H 'Content-Type: text/plain' 'https://pastr.xditya.me/api/v1/pastes?lang=markdown&expires=1d&burn'
# A form works too (curl -F) — the filename picks the language
curl -F 'content=@script.py' -F expires=1h https://pastr.xditya.me/api/v1/pastes
# Images (png, jpeg, gif, webp, up to 700 KB) go the same way; /raw serves them back with their media type
curl --data-binary @shot.png -H 'Content-Type: image/png' 'https://pastr.xditya.me/api/v1/pastes?expires=1d'The edit token is returned in the X-Edit-Token header (and in the JSON body). Keep it if you want to change or delete the paste later. Terminal clients (curl, wget, httpie) get the URL back as text and errors as error: message (code); force a format with Accept: application/json, Accept: text/plain, or ?plain. Send Content-Type: text/plain for raw bodies so a file that happens to start with content= is not read as a form.
Create
POST /api/v1/pastes
Content-Type: application/json
{
"content": "hello", // required, ≤ 1.00 MB
"title": "hello.txt", // optional, ≤ 120 chars
"lang": "go", // optional; id or alias (unknown → "text"; auto-detection is a web-editor feature)
"expires": "7d", // 10m | 1h | 1d | 7d | 30d | never
"burn": false, // destroy after the first read (true/false, 1/0, yes/no)
"enc": { ... } // present only for client-encrypted pastes, see below
}
201 Created
{
"id": "k7Pq2Xw9", "url": "https://pastr.xditya.me/k7Pq2Xw9.go", "rawUrl": "https://pastr.xditya.me/k7Pq2Xw9/raw",
"editToken": "…", // shown once, never stored in clear
"title": "hello.txt", "lang": "go", "created": 1757000000000, "expires": 1757604800000,
"burn": false, "views": 0, "size": 5
}Bodies may also be multipart/form-data, application/x-www-form-urlencoded or raw text. For raw text, pass options as query parameters (name, lang, expires, burn, title). In JSON, an image is its base64 in content with lang set to png, jpeg, gif or webp; raw and multipart bodies with an image content type or file name are converted for you.
Read
GET /api/v1/pastes/:id → JSON (counts a view)
GET /:id/raw → text/plain, or the image bytes with their media type (counts a view)
GET /:id/raw?dl=1 → download with a filename
GET /:id.go → web view highlighted as Go, whatever the stored language
HEAD any of the above → existence check only: never counts a view, never burns
Every GET that returns content destroys a burn-after-read paste — including /raw, /raw/:key and
/documents/:key. Only the web page (/:id) shows a confirmation first.Update & delete
PATCH /api/v1/pastes/:id Authorization: Bearer <editToken>
{ "content": "…", "title": "…", "lang": "…" } (any subset; expiry is kept; "title": "" clears it)
DELETE /api/v1/pastes/:id Authorization: Bearer <editToken> → 204Client-side encryption
When you turn on Encrypt in the editor, the browser encrypts a JSON envelope {title, lang, content} with AES-256-GCM and uploads only the ciphertext. In link mode the 256-bit key is appended to the URL after #, which browsers never send to servers. In password mode the key is derived with PBKDF2-SHA256 (600,000 iterations) and a random salt.
{
"content": "<base64url ciphertext>",
"enc": { "alg": "AES-GCM", "kdf": "fragment" | "password", "iv": "<base64url>", "salt"?: "<base64url>", "iterations"?: 600000 }
}The server cannot read encrypted pastes, so their title and language are hidden too, and /raw returns the ciphertext with an X-Encrypted: 1 header.
hastebin compatibility
POST /documents → { "key": "k7Pq2Xw9" } (30-day expiry)
GET /documents/:key → { "data": "…", "key": "…" }
GET /raw/:key → text/plainPoint any hastebin client (haste, wgetpaste, editor plugins) at this host and it keeps working.
Limits & errors
- Content up to 1.00 MB; images up to 700 KB before base64; titles up to 120 characters.
- Rate limits per IP: 20 creates/min, 120 reads/min, 30 edits or deletes/min, 5 reports/10 min. Over the limit you get 429 with a Retry-After header.
- Errors are { "error": { "code", "message" } } with the matching HTTP status (400 invalid, 401/403 token problems, 404 missing or expired, 413 too large, 429 rate limited).
- Paste pages are never indexed by search engines. Expiry is enforced by the database, not a cron job, so it is exact.
Operator API
# Set ADMIN_TOKEN in the environment, then:
curl -H 'Authorization: Bearer $ADMIN_TOKEN' 'https://pastr.xditya.me/api/v1/admin/pastes?limit=50' # newest first
curl -H 'Authorization: Bearer $ADMIN_TOKEN' 'https://pastr.xditya.me/api/v1/admin/pastes?sort=reports' # most reported
curl -X DELETE -H 'Authorization: Bearer $ADMIN_TOKEN' https://pastr.xditya.me/api/v1/pastes/AbCd1234 # remove anythingListings never include content. Reports are kept for 30 days with a salted hash of the reporter, and optionally forwarded to REPORT_WEBHOOK_URL. Set MAX_EXPIRY (for example 30d) to cap how long pastes may live.
Security model
- Plain pastes are readable by anyone with the link (and by the operator). Links are unguessable 8-character ids from a 55-symbol alphabet, never indexed, and expire exactly when you said.
- Encrypted pastes are sealed in your browser with AES-256-GCM before upload. In link mode the key lives after #, which browsers never send to servers; in password mode it is derived with PBKDF2-SHA256 (600,000 iterations). The server stores ciphertext, an IV and a salt, and learns only the size. This protects against a database leak or a curious operator. It does not protect against someone who has the full link, a compromised browser or extension, or a malicious copy of this site's JavaScript — check the source or self-host if that matters to you.
- Burn after read deletes the paste in the same atomic step that reads it, so two readers can never both see it. Link previewers only ever see the confirmation page; HEAD requests are side-effect free.
- Edit tokens are 256-bit random values shown once; only a SHA-256 hash is stored. Your browser keeps them (and link keys) in localStorage for the “Your pastes” list — export that list if you clear site data.
- What the server keeps: the paste, its metadata (language, size, timestamps, view count), rate-limit counters keyed by IP for a few minutes, and hashed reporter ids on abuse reports. No analytics, no third-party scripts, no SDK telemetry, Referrer-Policy: no-referrer, a nonce-based CSP.
- Before you paste a secret: the editor warns when text looks like a key or password. Prefer encrypt + burn, or better, don't paste it at all.
Capabilities
GET /api/v1/info → { name, version, limits, expiries, defaultExpiry, languages, features, stats }