Changelog
Per-API-change history. Each entry corresponds to a deployment that touched the public API surface. For the broader project roadmap and shipped features, see /backlogs.
2026-10-11 — Redirect links, live content + reserved slugs
ADDITIVE
- POST /api/v1/links — new body shape {"type": "redirect", "redirect_url", "slug"?, "label"?, "tags"?, "expires_at"?}: creates a link that answers 302 to redirect_url (https only, no credentials, default port, host on the environment allowlist). The file body is unchanged and stays the default when type is absent
- POST /api/v1/links — optional slug on redirect bodies, accepted only from API consumers OCPL has enabled for a slug prefix: wf- followed by 8 lowercase hex characters (11 characters in all)
- DELETE /api/v1/links/{slug}, PATCH /api/v1/links/{slug}/tags and DELETE /api/v1/links/{slug}/tags/{tag} — the slug path parameter also accepts the 11-character wf- form
- Error codes: invalid_redirect_url, redirect_host_not_allowed, invalid_slug, slug_not_allowed, slug_taken. Error bodies may carry an optional field naming the offending request field
- POST /api/v1/links — new body shape {"type": "dynamic", "api_url", "slug"?, "label"?, "tags"?, "expires_at"?}: a live content link. At request time the short URL fetches api_url (https, default port, a domain name on the upstream allowlist, with the upstream token of your consumer) and streams the answer back
- Public GET on a live content link — only a 200 is shown, streamed with just Content-Type, Content-Disposition and Content-Length from the upstream (25 MB cap, 10 s to the response headers, never cached, redirects never followed); only PDF, PNG, JPEG, GIF, WebP, plain text and JSON are shown in the browser, any other type (HTML and SVG included) is always sent as a download; upstream 404 and 410 show the branded pages, anything else a temporarily-unavailable page (502 or 504)
- Error codes: invalid_api_url, api_host_not_allowed, no_upstream_token
- expires_at — link expiry is not implemented, so links never expire. Redirect and live content bodies accept "expires_at": null (it means never expires and changes nothing, like leaving it out); any other value returns 422 invalid_field until expiry exists. File bodies do not accept expires_at
2026-05-05 — Plan 4c · Public docs + OpenAPI
ADDITIVE
- GET /api/v1/openapi.json (new — public, no auth, 5-min edge cache)
- Internal: every /api/v1 parser refactored onto src/schema/* (byte-identical behaviour; no caller-visible changes)
2026-05-04 — Plan 4b · Tags
ADDITIVE
- POST /api/v1/links — body field tags?: string[] (max 20, charset [a-z0-9:_-])
- POST /api/v1/links — response field tags: string[] (always present, [] if none)
- PATCH /api/v1/links/{slug}/tags (new — additive, never removes)
- DELETE /api/v1/links/{slug}/tags/{tag} (new)
- Error codes: invalid_tag, too_many_tags
2026-04-25 — Plan 2 · API for create + revoke + webhooks
INITIAL RELEASE
- POST /api/v1/links (create)
- DELETE /api/v1/links/{slug} (revoke)
- Idempotency-Key support (24h TTL, conflict detection)
- Webhook delivery on revoke (5s timeout, fire-and-forget)