- Backend create/can validation paths emit machine-readable error codes (slug_taken, slug_invalid, content_empty, content_too_large, expiry_invalid, rate_limited, ...) alongside the human message - new.html JS maps codes to plain-language guidance with generic fallback - Result card colored via --ok/--err/--warn left border (result-ok/err/warn) - docs/API.md error section documents the code field - Tests assert the code on every validation path
137 lines
4.6 KiB
Markdown
137 lines
4.6 KiB
Markdown
# Palette API
|
||
|
||
All endpoints are JSON unless noted. The web UI is served from the same port.
|
||
|
||
## Create paste
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/pastes \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"content": "print(hello)",
|
||
"title": "my snippet",
|
||
"language": "python",
|
||
"expires_in": "168h",
|
||
"password": "optional",
|
||
"custom_slug": "optional",
|
||
"burn_after_read": false,
|
||
"burn_after_reads": 1,
|
||
"visibility": "public"
|
||
}'
|
||
```
|
||
|
||
- `expires_in` is a Go duration string (`90m`, `6h`, `336h`). Omit for no expiry.
|
||
- `visibility` is `public` or `unlisted`.
|
||
- Alternatively (or additionally), `public` may be sent as a boolean (#83):
|
||
`false` maps to `unlisted` and `true` maps to `public`. When both fields are
|
||
present, the boolean `public` takes precedence over the string `visibility`.
|
||
Omitting both defaults to `public`.
|
||
- `burn_after_reads` sets how many reads the paste survives (default 1 when
|
||
`burn_after_read` is true). A read is counted per unique viewer session;
|
||
the same viewer returning within 15 minutes does not count again.
|
||
- Response includes `id`, `url`, `raw_url`, `api_url`, `expires_at`,
|
||
`created_at`, and a one-time `deletion_token`.
|
||
|
||
Errors return JSON with a human-readable `error` message plus a
|
||
machine-readable `code` the web UI maps to plain-language guidance (#105):
|
||
|
||
| Code | Status | Meaning |
|
||
|---|---|---|
|
||
| `content_empty` | 400 | content is required |
|
||
| `slug_invalid` | 400/409 | custom slug malformed |
|
||
| `slug_taken` | 409 | custom slug already in use |
|
||
| `slug_reserved` | 409 | custom slug is reserved |
|
||
| `expiry_invalid` | 400 | expires_in out of 1 minute – 1 year range |
|
||
| `content_too_large` | 413 | content or body exceeds the size cap |
|
||
| `rate_limited` | 429 | too many requests; see `Retry-After` |
|
||
|
||
Other statuses: `400` invalid body, `401` password required, `404` paste
|
||
expired/burned/gone. Unknown codes should be treated as a generic failure.
|
||
|
||
## Get paste
|
||
|
||
```bash
|
||
curl http://localhost:8080/api/pastes/{id}
|
||
# password-protected pastes:
|
||
curl "http://localhost:8080/api/pastes/{id}?password=secret"
|
||
# or via header: X-Paste-Password: secret
|
||
```
|
||
|
||
The response includes `reads_remaining` (`null` when no read budget is set).
|
||
|
||
## Raw content
|
||
|
||
```bash
|
||
curl http://localhost:8080/raw/{id}
|
||
```
|
||
|
||
Raw reads count against a burn-after-read budget, same as page views.
|
||
|
||
## Delete
|
||
|
||
```bash
|
||
# soft delete (requires the deletion token from the create response)
|
||
curl -X DELETE -H "Authorization: Bearer TOKEN" http://localhost:8080/api/pastes/{id}
|
||
# ...or via query param; the creator browser (viewer cookie) may also delete without a token
|
||
curl -X DELETE "http://localhost:8080/api/pastes/{id}?token=TOKEN"
|
||
|
||
# hard delete immediately (requires the one-time deletion token)
|
||
curl -X DELETE "http://localhost:8080/api/pastes/{id}/redeem?token=TOKEN"
|
||
```
|
||
|
||
## Lists
|
||
|
||
```bash
|
||
curl "http://localhost:8080/api/public?limit=25&offset=0" # public history
|
||
curl http://localhost:8080/api/mine # this browser's pastes (viewer cookie)
|
||
```
|
||
|
||
## Language detection
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/guess-language \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"content": "package main"}'
|
||
```
|
||
|
||
## Cans (bundles of items)
|
||
|
||
A can bundles multiple text items (and files) into one shareable page at
|
||
`/can/{id}`. Password, expiry, visibility, custom slug, and viewer-scoped
|
||
deletion work exactly like pastes; cans appear as normal rows (with a `can`
|
||
badge) in `/api/public` and `/api/mine`.
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/pastes/can \
|
||
-F "title=My bundle" \
|
||
-F "expires_in=48h" \
|
||
-F "custom_slug=my-bundle" \
|
||
-F 'json_items=[{"title":"notes.txt","content":"some notes"}]' \
|
||
-F "files=@screenshot.png" \
|
||
-F "files=@log.txt"
|
||
|
||
curl http://localhost:8080/api/cans/{id}
|
||
curl http://localhost:8080/api/cans/{id}/items/{item_id}
|
||
curl -X DELETE http://localhost:8080/api/cans/{id} # creator browser only (vwr cookie)
|
||
```
|
||
|
||
Password-protected cans use the same unlock flow as pastes: `POST /can/{id}`
|
||
with the password sets an HMAC-bound cookie; item fetches accept the cookie as
|
||
well as the `X-Paste-Password` header / `?password=` query param.
|
||
|
||
## Web pages
|
||
|
||
- `/new` — create a paste
|
||
- `/history` — public paste history
|
||
- `/mine` — pastes created from this browser
|
||
- `/{id}` — view a paste
|
||
- `/unlock/{id}` — password gate for protected pastes
|
||
- `/raw/{id}` — raw content with original content type
|
||
|
||
## Expiry and deletion
|
||
|
||
- Expired pastes are soft-deleted by a background sweeper (runs every minute).
|
||
- Soft-deleted pastes are hard-deleted after a 7-day grace period.
|
||
- Deletion tokens allow immediate hard delete.
|
||
- Burn-after-read pastes are soft-deleted once the read budget is exhausted.
|