openapi: 3.0.3 info: title: GoLinks API description: | # GoLinks — Link & Content Management API GoLinks is a self-hosted link management system. It lets you create short links, bookmark pages with auto-extracted metadata, manage image collections, upload files, write blog posts, and serve random images from a personal library. ## Key concepts | Resource | What it is | |---|---| | **Page** | A bookmarked URL. When created, the server asynchronously fetches its title, description, and a screenshot. | | **Bookmark** | An archived social-media post (e.g. an x.com / Twitter tweet) saved with full preview data — text, author, images, video poster thumbnails, link cards, quotes — so the archive page renders offline. Bulk-import via `/api/bookmarks/import`. | | **Post** | A Markdown blog post, optionally tagged. | | **Collection** | A named group of uploaded images (e.g. a photo album). | | **Image** | An image file belonging to a Collection. | | **File** | A generic uploaded file. Files can be made public via a shareable token URL. | | **Random image** | Serves a random image from the server's local image library, resized on the fly. Great for placeholder images. | ## Common patterns ### Pagination All list endpoints accept `?page=` (1-based) and `?page_size=` query parameters. Responses follow the envelope `{ count, next, previous, results[] }`. ### Image embedding Use the random image endpoint as a live placeholder: ``` ![placeholder](https://your-domain/api/images/random/400/300/?fit=crop&v=1234567890) ``` Or import and self-host an external image: ``` ![photo](https://your-domain/import/images/example.com/path/photo.jpg) ``` ### File sharing Upload a file via `POST /api/files`, call `POST /api/files/{id}/toggle-public` to make it public, then share the returned `public_url` (`/public/files/{id}-{filename}`) — no authentication is required for the recipient. Set an expiry with `POST /api/files/{id}/set-expiry` to time-limit the link. ### Bookmarks bulk import Import (or upsert) many archived posts in one request. Each item accepts the full normalised export object from `x-bookmarks-exporter` (with nested `author`, `media`, `card`, `quoted`, `retweet`, counters); matching is by `tweet_id`: ``` POST /api/bookmarks/import { "bookmarks": [ { "id": "123", "author": {"screen_name":"u"}, "text": "...", "media": [...], ... }, ... ] } → { "created": 10, "updated": 0, "skipped": 0, "total": 769 } ``` Use `PUT /api/bookmarks/bulk` with a bare JSON array of flat `Bookmark`-shaped objects for the same upsert behaviour. ## Authentication No authentication is currently required on any endpoint. version: 1.0.0 x-logo: url: /static/images/logo.png servers: - url: http://localhost:8000 description: Local development server tags: - name: Links description: Short links with click tracking. Supports listing all links and retrieving the most visited ones sorted by click count. - name: Pages description: | Bookmarked web pages. Creating a page triggers an async background job that fetches the page title, description, and screenshot automatically. - name: Posts description: Markdown blog posts with optional tag categorisation. - name: Bookmarks description: | Archived social-media posts (originally x.com / Twitter bookmarks), each saved with full preview data — text, author, media poster thumbnails (no embedded video streams), link cards, quotes — so the archive viewer page renders offline without further API calls. Use `POST /api/bookmarks/import` for bulk upsert by `tweet_id`. - name: Collections description: Named groups of images. Each collection can hold many uploaded image files. - name: Images description: | Image management and random image serving. The random image endpoint is useful as a live placeholder — pass `?v={timestamp}` to bust the browser cache on each render. - name: Files description: | Generic file uploads with optional public sharing via tokenised URLs. Files can be made public/private and given an expiry date. paths: /api/links: get: operationId: listLinks tags: [Links] summary: List all links description: Retrieve a paginated list of all short links ordered by creation date (newest first). parameters: - in: query name: page schema: type: integer default: 1 description: Page number (1-based). - in: query name: page_size schema: type: integer default: 10 maximum: 100 description: Number of results per page. responses: '200': description: Paginated list of links. content: application/json: schema: $ref: '#/components/schemas/LinkList' /api/links/most-visited: get: operationId: listMostVisitedLinks tags: [Links] summary: Most visited links description: Return links that have at least one click, sorted by click count descending. parameters: - in: query name: page schema: type: integer default: 1 description: Page number (1-based). - in: query name: page_size schema: type: integer default: 10 maximum: 100 description: Number of results per page. responses: '200': description: Paginated list of links sorted by click_count descending. content: application/json: schema: $ref: '#/components/schemas/LinkList' /api/pages: get: operationId: listPages tags: [Pages] summary: List all pages description: Retrieve a paginated list of all bookmarked pages. parameters: - in: query name: page schema: type: integer description: Page number for pagination - in: query name: page_size schema: type: integer description: Number of items per page responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/PageList' post: operationId: createPage tags: [Pages] summary: Create a new page description: | Bookmark a URL. After creation the server starts an async background job that fetches the page title, description, content, and a screenshot. Poll `GET /api/pages` or check `processing_status` to see when it completes. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PageCreate' responses: '201': description: Page created successfully content: application/json: schema: $ref: '#/components/schemas/Page' /api/bookmarks: get: operationId: listBookmarks tags: [Bookmarks] summary: List archived bookmarks description: | Paginated list of archived social-media bookmarks, newest first. Supports full-text search (`q`), boolean media/type filters, author filter and ordering. Default page size is 24; use `?page_size=` up to 200. parameters: - in: query name: page schema: { type: integer } description: 1-based page number - in: query name: page_size schema: { type: integer, maximum: 200 } description: Items per page (default 24) - in: query name: q schema: { type: string } description: Case-insensitive search across `text`, `summary`, `author_screen_name`, `author_name` - in: query name: has_video schema: { type: boolean } description: Filter to bookmarks containing a video/animated GIF - in: query name: has_photo schema: { type: boolean } - in: query name: has_card schema: { type: boolean } description: Has an attached link preview card - in: query name: is_quote schema: { type: boolean } - in: query name: is_retweet schema: { type: boolean } - in: query name: author schema: { type: string } description: Exact (case-insensitive) screen name - in: query name: ordering schema: type: string enum: [tweet_created_at, -tweet_created_at, bookmark_created_at, -bookmark_created_at, favorite_count, -favorite_count, view_count, -view_count, created_at, -created_at] description: Sort field (prefix `-` for descending). Default `-tweet_created_at` responses: '200': description: Paginated bookmarks content: application/json: schema: $ref: '#/components/schemas/BookmarkList' post: operationId: createBookmark tags: [Bookmarks] summary: Create a single bookmark description: | Create one archived bookmark from a flat `Bookmark`-shaped object. For importing many at once (e.g. from x-bookmarks-exporter), prefer `POST /api/bookmarks/import`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BookmarkCreate' responses: '201': description: Bookmark created content: application/json: schema: $ref: '#/components/schemas/Bookmark' /api/bookmarks/stats: get: operationId: bookmarkStats tags: [Bookmarks] summary: Aggregate bookmark counts description: Returns total count plus per-flag counts (used by the viewer's filter chips). responses: '200': description: Aggregate stats content: application/json: schema: type: object properties: total: { type: integer } has_video: { type: integer } has_photo: { type: integer } has_card: { type: integer } is_quote: { type: integer } is_retweet: { type: integer } /api/bookmarks/import: post: operationId: importBookmarks tags: [Bookmarks] summary: Bulk upsert bookmarks from export objects description: | Upserts many bookmarks in one request. Each item in `bookmarks` accepts the full normalised export object from `x-bookmarks-exporter` — with nested `author`, `media`, `card`, `quoted`, `retweet`, engagement counters and `created_at` (x.com-style RFC date string). Matching is by `tweet_id` (top-level `id` alias also accepted); existing entries are updated, new ones created. Returns `{ created, updated, skipped, total }`. Example body: ```json { "bookmarks": [ { "id": "1234567890", "url": "https://x.com/user/status/1234567890", "author": {"screen_name":"user","name":"User","profile_image_url":"..."}, "text": "Hello world", "media": [{"type":"video","media_url":"...","video":{"poster":"...","best_mp4":"...","duration_ms":12000}}], "favorite_count": 5, "view_count": 99 } ] } ``` requestBody: required: true content: application/json: schema: type: object required: [bookmarks] properties: bookmarks: type: array items: $ref: '#/components/schemas/BookmarkExportItem' responses: '200': description: Upsert summary content: application/json: schema: type: object properties: created: { type: integer } updated: { type: integer } skipped: { type: integer } total: { type: integer, description: Total bookmarks now in DB } /api/bookmarks/bulk: put: operationId: bulkUpsertBookmarks tags: [Bookmarks] summary: Bulk upsert from a flat JSON array description: | Like `/import` but accepts a bare JSON array (not wrapped in `{bookmarks:[]}`). Each item can be a flat `Bookmark`-shaped object (matching `BookmarkCreate`/`Bookmark`) or a normalised export object; `tweet_id` (or top-level `id`) is used as the upsert key. requestBody: required: true content: application/json: schema: type: array items: $ref: '#/components/schemas/BookmarkCreate' responses: '200': description: Upsert summary content: application/json: schema: type: object properties: created: { type: integer } updated: { type: integer } skipped: { type: integer } total: { type: integer } post: operationId: bulkUpsertBookmarksPost tags: [Bookmarks] summary: Bulk upsert from a flat JSON array (POST alias) description: Alias of `PUT /api/bookmarks/bulk` for clients that cannot send PUT. requestBody: required: true content: application/json: schema: type: array items: $ref: '#/components/schemas/BookmarkCreate' responses: '200': description: Upsert summary content: application/json: schema: type: object properties: created: { type: integer } updated: { type: integer } skipped: { type: integer } total: { type: integer } /api/bookmarks/{id}: parameters: - in: path name: id required: true schema: { type: integer } description: Internal DB primary key (NOT the tweet_id) get: operationId: retrieveBookmark tags: [Bookmarks] summary: Retrieve a single bookmark responses: '200': description: Bookmark detail content: application/json: schema: $ref: '#/components/schemas/Bookmark' '404': description: Not found patch: operationId: updateBookmark tags: [Bookmarks] summary: Partially update a bookmark description: | Update any subset of fields. Common use by agents: write an AI-generated `summary` for the post via `{"summary": "..."}`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BookmarkCreate' responses: '200': description: Updated bookmark content: application/json: schema: $ref: '#/components/schemas/Bookmark' delete: operationId: deleteBookmark tags: [Bookmarks] summary: Delete a bookmark responses: '204': description: Deleted '404': description: Not found /api/posts: get: operationId: listPosts tags: [Posts] summary: List all posts description: Retrieve a paginated list of all Markdown blog posts. parameters: - in: query name: page schema: type: integer description: Page number for pagination - in: query name: page_size schema: type: integer description: Number of items per page responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/PostList' post: operationId: createPost tags: [Posts] summary: Create a new post description: Create a Markdown blog post. Pass tag slugs in the `tags` array to categorise it. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PostCreate' responses: '201': description: Post created successfully content: application/json: schema: $ref: '#/components/schemas/Post' /api/collections: get: operationId: listCollections tags: [Collections] summary: List all collections description: Retrieve a paginated list of all image collections. parameters: - in: query name: page schema: type: integer description: Page number for pagination - in: query name: page_size schema: type: integer description: Number of items per page responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/CollectionList' post: operationId: createCollection tags: [Collections] summary: Create a new collection description: Create a new named image collection. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CollectionCreate' responses: '201': description: Collection created successfully content: application/json: schema: $ref: '#/components/schemas/CollectionResponse' /api/collections/{collection_id}: delete: operationId: deleteCollection tags: [Collections] summary: Delete a collection description: Permanently delete a collection and all its images. parameters: - in: path name: collection_id required: true schema: type: string format: uuid description: The ID of the collection to delete responses: '200': description: Collection deleted successfully content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' /api/collections/{collection_id}/upload_images: post: operationId: uploadImagesToCollection tags: [Collections] summary: Upload images to a collection description: | Upload one or more images to a collection. Only `image/*` content types are accepted. Optionally provide a parallel `descriptions[]` array — each entry maps to the image at the same index. parameters: - in: path name: collection_id required: true schema: type: string format: uuid description: The ID of the collection to upload images to requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: array items: type: string format: binary description: List of image files to upload. Only image/* content types are allowed. descriptions: type: array items: type: string description: Optional list of descriptions for the uploaded images. Each description corresponds to the image at the same index. required: - file responses: '201': description: Images uploaded successfully content: application/json: schema: type: object properties: status: type: string enum: [success] example: success message: type: string example: "Successfully uploaded 2 images" data: type: array items: $ref: '#/components/schemas/Image' /api/images/{image_id}: delete: operationId: deleteImage tags: [Images] summary: Delete an image description: Permanently delete a single image from its collection. parameters: - in: path name: image_id required: true schema: type: string format: uuid description: The ID of the image to delete responses: '200': description: Image deleted successfully content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' /api/files: get: operationId: listFiles tags: [Files] summary: List all uploaded files description: Retrieve a paginated list of all uploaded files. parameters: - in: query name: page schema: type: integer description: Page number for pagination responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/FileList' post: operationId: uploadFiles tags: [Files] summary: Upload one or more files description: | Upload one or multiple files via `multipart/form-data`. Files are stored server-side in the configured `FILE_UPLOADS_FOLDER`. Returns an array of `FileUpload` objects. requestBody: required: true content: multipart/form-data: schema: type: object required: - files properties: files: type: array items: type: string format: binary description: One or more files to upload responses: '201': description: Files uploaded successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/FileUpload' '400': description: No files provided '500': description: >- None of the uploaded files could be fully persisted to storage (e.g. a storage write failure or truncated upload). The response body includes an `errors` array with a `name`/`error` entry per failed file. /api/files/{id}: get: operationId: getFile tags: [Files] summary: Get file details description: Fetch metadata for a single file by its UUID. parameters: - in: path name: id required: true schema: type: string format: uuid responses: '200': description: File details content: application/json: schema: $ref: '#/components/schemas/FileUpload' '404': description: File not found delete: operationId: deleteFile tags: [Files] summary: Delete a file description: Permanently delete the file record and remove it from disk. parameters: - in: path name: id required: true schema: type: string format: uuid responses: '204': description: File deleted '404': description: File not found /api/files/{id}/download: get: operationId: downloadFile tags: [Files] summary: Download or view a file description: | Serve the raw file bytes. Images, videos, and audio are returned with `Content-Disposition: inline` (viewable in browser); all other files use `attachment` (triggers download). **HTTP Range requests are supported.** Send a `Range: bytes=start-end` header to fetch a byte range; the server replies with `206 Partial Content`, a `Content-Range: bytes start-end/total` header, and `Accept-Ranges: bytes`. This is what enables seeking inside `