Skip to content

Pastepile API

A small, public REST API for creating, reading, listing, updating, and deleting pastes from scripts, CI jobs, webhooks, and your terminal.

  • No API key. No account. Anonymous by default.
  • Optional, free API key adds per-key rate limits and a private, searchable namespace for your pastes.
  • CORS-open for every endpoint.
  • Editable via a one-shot edit key returned at creation.
  • Machine-readable OpenAPI 3.1 spec.

Base URL

https://www.pastepile.com

Endpoints at a glance

POST    /api/paste                    pipe endpoint: raw body in, paste URL out
POST    /api/public/pastes            create a paste (returns edit_key, shown once)
GET     /api/public/pastes            list pastes (public feed, or scope=mine with a key)
GET     /api/public/pastes/{slug}     fetch one paste as JSON
PATCH   /api/public/pastes/{slug}     update title/files (auth: edit_key)
PUT     /api/public/pastes/{slug}     alias of PATCH
DELETE  /api/public/pastes/{slug}     delete the paste (auth: edit_key)
GET     /raw/{slug}                   fetch raw text (text/plain)
POST    /api/keys                     generate an API key (optionally pro via pro_key)
POST    /api/keys/revoke              revoke a key with its revocation_secret
GET     /api/openapi.json             OpenAPI 3.1 spec

Pipe from your terminal

One-liner paste sharing, no JSON required. Send anything to /api/paste and get the paste URL back as plain text. Pastes made this way are unlisted (link-only) and expire in 1 week; the one-time edit_key comes back in the X-Edit-Key response header.

# pipe a command's output
dmesg | curl -sF 'p=<-' https://www.pastepile.com/api/paste

# send a file
curl -s --data-binary @error.log https://www.pastepile.com/api/paste

# handy alias
alias pp="curl -sF 'p=<-' https://www.pastepile.com/api/paste"
history | pp

Limits & notes

  • Total content per paste: 2 MB across all files for anonymous and free-key requests (4 MB for time-capsule pastes); pro keys allow 25 MB, enterprise 100 MB.
  • Files per paste: 1 to 10. Filenames: 1 to 80 chars.
  • Create rate limit (keyless): 30 / hour / IP, with a short burst cap of 30 / minute / IP. With an API key, creates are governed by the per-key limits 30 requests / minute and 500 creates / day. Update: 60/hour/paste/IP. Delete: 20/hour/paste/IP.
  • Key generation: 5 new keys / day / IP. Key revocation attempts: 20 / hour / IP.
  • The API creates non-encrypted pastes only. End-to-end encryption and time-capsule (drand timelock) pastes require the browser-held key and are not available over HTTP.
  • Password-protected, encrypted, and sealed time-capsule pastes are not returned by the read endpoint.
  • Errors are JSON: {"error":{"code":"...","message":"..."}}

Limits (per key plan)

Keys are free and minted on the free plan. Linking a Pastepile Pro access key at creation upgrades the key to the pro plan; if the subscription ends, the key returns to free automatically. Anonymous keyless traffic keeps the "Limits & notes" rules above, including every expiry option.

PlanCreates / dayRequests / minMax pasteNo-expiry
Free500302 MBNo
Pro2,00012025 MBYes
Enterprise10,000600100 MBYes

On a free key, an explicit expiry: "never" is rejected with an actionable message rather than silently downgraded. Rate-limited responses expose X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After headers.

API keys (optional, free)

Keys are optional. The keyless API works exactly as documented here. A key buys two things: per-key rate limits instead of shared per-IP ones, and a private namespace: pastes saved with your key become listable and searchable via scope=mine, including unlisted ones. No sign-up required.

  • Generate one on the keys page or via POST /api/keys.
  • Send it as X-API-Key: pk_live_... on requests.
  • You can also send it as Authorization: Bearer pk_live_... on the create endpoint.
  • Generation returns BOTH the key and a revocation_secret (rvk_...), each shown once. We store only their SHA-256 hashes.
  • Keys are independent: generating a new key does NOT revoke any other key.
  • Optional pro_key in the body: pass your Pro access key and the minted API key runs on the pro plan (the response includes "plan"). An invalid Pro access key fails loudly with 402 invalid_pro_key instead of silently minting a free key.
  • If you lose the key, generate a new one. If you lose the revocation secret too, contact the operator with the key prefix.
# generate a key (returns key AND revocation_secret, both ONCE)
curl -X POST "https://www.pastepile.com/api/keys" \
  -H "Content-Type: application/json" \
  -d '{"label": "CI bot"}'

# use the key on a create
curl -X POST "https://www.pastepile.com/api/public/pastes" \
  -H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: text/plain" \
  --data-binary "hello"

# revoke a key with its revocation secret
curl -X POST "https://www.pastepile.com/api/keys/revoke" \
  -H "Content-Type: application/json" \
  -d '{"revocation_secret": "rvk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}'
# 200 { "ok": true, "key_prefix": "pk_live_ab12", "label": "CI bot", ... }
# 404 { "ok": false, "message": "No matching key. ..." }

Authentication (edit key)

Creating a paste returns an edit_key in the JSON response. It is shown once. The server only stores its SHA-256 hash; if you lose it you cannot recover it. The edit key is per-paste and is separate from your optional API key. Send it on update/delete one of three ways:

Authorization: Bearer <edit_key>
# or
X-Edit-Key: <edit_key>
# or in the JSON body
{ "edit_key": "<edit_key>", ... }

POST /api/public/pastes

Create a paste. Two content types are accepted.

JSON body

POST https://www.pastepile.com/api/public/pastes
Content-Type: application/json

{
  "title":       "Optional title (<= 120 chars)",
  "content":     "string  (single-file shortcut)",
  "language":    "plaintext | javascript | typescript | python | java | c | cpp | csharp | go | rust | ruby | php | html | css | sql | json | yaml | bash | markdown",
  "files":       [ { "name": "main.js", "language": "javascript", "content": "..." } ],
  "expiry":      "never | 10m | 1h | 1d | 1w | 1mo | burn",
  "visibility":  "public | unlisted",
  "password":    "optional server-side password",
  "custom_slug": "optional vanity URL: 3-40 chars, lowercase letters, digits, single hyphens"
}

Provide either content (single file) or a non-empty files array. Default expiry is 1w, default visibility is public.burn forces unlisted and deletes the paste on first read. The full list of syntax-highlighting languages lives at /languages.

Plain-text body (shell-friendly)

POST https://www.pastepile.com/api/public/pastes
Content-Type: text/plain

<the paste content>

Response body is just the paste URL followed by a newline.

Response (JSON body)

200 OK
{
  "slug":             "ab12cd3",
  "url":              "https://www.pastepile.com/p/ab12cd3",
  "raw_url":          "https://www.pastepile.com/raw/ab12cd3",
  "edit_key":         "kK9...32-char-secret",
  "edit_key_notice":  "Save this edit_key now - it is shown once and is required to update or delete this paste."
}

Create with multiple files

curl -X POST "https://www.pastepile.com/api/public/pastes" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Two files",
    "expiry": "1d",
    "visibility": "public",
    "files": [
      { "name": "main.js",  "language": "javascript", "content": "console.log(1)" },
      { "name": "README.md","language": "markdown",   "content": "# hi"           }
    ]
  }'

GET /api/public/pastes

List recent pastes. Default scope is the public feed (no unlisted, encrypted, or sealed content). With scope=mine and an API key, it lists YOUR pastes instead: the ones saved with that key, newest first, including unlisted ones. search= filters either scope by title, filename, and content keywords.

GET https://www.pastepile.com/api/public/pastes?limit=20&offset=0&search=

200 OK
{
  "items": [
    { "slug": "ab12cd3", "title": "...", "language": "javascript",
      "preview": "...", "views": 4, "file_count": 1,
      "created_at": "...", "url": "https://www.pastepile.com/p/ab12cd3", "raw_url": "https://www.pastepile.com/raw/ab12cd3" }
  ],
  "total": 123, "limit": 20, "offset": 0
}

# your private namespace (requires X-API-Key or Authorization: Bearer)
GET https://www.pastepile.com/api/public/pastes?scope=mine&search=runbook
# 200 { "items": [...], "total": 3, "limit": 20, "offset": 0, "scope": "mine" }
# 401 without a valid key: scope=mine requires authentication

Isolation is per key: one key can never list or search another key's pastes. Previews are never returned for encrypted, password-protected, or burn-after-read pastes.

GET /api/public/pastes/{slug}

Fetch one paste as JSON. Increments the view counter and consumes burn-after-read pastes (same semantics as the website).

GET https://www.pastepile.com/api/public/pastes/{slug}

200 OK
{
  "slug": "ab12cd3",
  "title": "Optional title",
  "language": "javascript",
  "files": [ { "name": "main.js", "language": "javascript", "content": "console.log('hi')" } ],
  "views": 4,
  "created_at": "2026-06-14T12:34:56Z",
  "expires_at": "2026-06-21T12:34:56Z",
  "burn_after_read": false
}

Returns 404 for missing/expired/password-protected/encrypted pastes, and 403 for sealed time capsules. Password hashes and edit keys are never returned.

PATCH /api/public/pastes/{slug} (or PUT)

Update title/files of an existing paste. Requires the edit key.

curl -X PATCH "https://www.pastepile.com/api/public/pastes/<slug>" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <edit_key>" \
  -d '{
    "title": "New title",
    "files": [ { "name": "main.js", "language": "javascript", "content": "console.log(2)" } ]
  }'

# 200 OK
# { "ok": true, "version_no": 3 }

DELETE /api/public/pastes/{slug}

Permanently delete a paste. Requires the edit key.

curl -X DELETE "https://www.pastepile.com/api/public/pastes/<slug>" \
  -H "Authorization: Bearer <edit_key>"

# 200 OK
# { "ok": true }

GET /raw/{slug}

Fetch the raw text of a paste as text/plain; charset=utf-8 with X-Content-Type-Options: nosniff. Ideal for piping into shell tools.

curl "https://www.pastepile.com/raw/{slug}"
# or pipe straight into a file
curl -o file.txt "https://www.pastepile.com/raw/{slug}"

Shell one-liner: pipe stdin, get a URL

Drop into .bashrc / .zshrc:

pv() {
  curl -s --data-binary @- \
    -H "Content-Type: text/plain" \
    "https://www.pastepile.com/api/public/pastes"
}

cat error.log | pv
git diff   | pv

Embeds

Every non-encrypted paste has a bare embed page at /embed/{slug}:

<iframe
  src="https://www.pastepile.com/embed/<slug>"
  style="width:100%;height:420px;border:0;border-radius:12px;overflow:hidden"
  loading="lazy"
  title="Pastepile paste"
></iframe>

OpenAPI

Machine-readable spec for codegen, Postman, Insomnia, or your favorite client generator:

GET https://www.pastepile.com/api/openapi.json

Errors

Errors are JSON with a stable code and a human message: 400 bad input, 401 missing/invalid edit key or API key, 402 invalid_pro_key on key generation, 403 sealed, 404 not found / protected, 409 custom_slug taken, 413 payload too large for your plan, 422 content rejected by the abuse filter, 429 rate limited (with Retry-After and X-RateLimit-* headers), 500 server error.