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. | | **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, call `POST /api/files/{id}/toggle-public` to get a public token, then share `/public/files/{token}/` — no authentication required for the recipient. ## 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: 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/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 /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 are returned with `Content-Disposition: inline` (viewable in browser); all other files use `attachment` (triggers download). parameters: - in: path name: id required: true schema: type: string format: uuid responses: '200': description: File content '404': description: File not found /api/files/{id}/toggle-public: post: operationId: toggleFilePublic tags: [Files] summary: Toggle public/private visibility description: | Toggle a file between public and private: - **Private → Public**: generates a `public_token` and returns a `public_url` (`/public/files/{token}/`) that anyone can access without authentication. - **Public → Private**: immediately revokes the token; existing `public_url` links stop working. parameters: - in: path name: id required: true schema: type: string format: uuid responses: '200': description: Updated visibility state content: application/json: schema: type: object properties: is_public: type: boolean public_token: type: string nullable: true public_url: type: string nullable: true example: /public/files// /api/files/{id}/set-expiry: post: operationId: setFileExpiry tags: [Files] summary: Set or clear expiry date for a public file description: | Set an expiry datetime for a public file. After the expiry the `public_url` returns 404. Send `expires_at: null` (or omit the field) to remove the expiry and make the link permanent. parameters: - in: path name: id required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: type: object properties: expires_at: type: string format: date-time nullable: true description: ISO 8601 datetime. Send null or omit to clear the expiry. responses: '200': description: Updated file details content: application/json: schema: $ref: '#/components/schemas/FileUpload' /public/files/{token}/: get: operationId: getPublicFile tags: [Files] summary: Access a public file by token description: | Unauthenticated endpoint. Serves a publicly shared file by its opaque token. Returns 404 if the file has been made private or if the expiry date has passed. parameters: - in: path name: token required: true schema: type: string responses: '200': description: File content (inline for images, attachment for others) '404': description: File not found, is private, or link has expired /api/images/random/: get: operationId: getRandomImage tags: [Images] summary: Get a random image description: | Returns a randomly selected image from the server's image library, resized to the default dimensions. Each request returns a different image. **Cache busting:** Add a `?v={timestamp}` query parameter to prevent browsers from caching repeated calls (the server ignores this parameter). parameters: - in: query name: fit schema: type: string enum: [clip, crop, fill, scale] default: scale description: | How to fit the image into the requested dimensions. - `scale` — scale to fit, maintaining aspect ratio (may add letterbox) - `crop` — crop to exact dimensions - `clip` — clip to fit, no upscaling - `fill` — fill exact dimensions, may distort - in: query name: v schema: type: integer description: Cache-busting value (e.g. Unix timestamp). Ignored by the server. example: 1774152070 responses: '200': description: Image file (JPEG or PNG) headers: Cache-Control: schema: type: string description: Caching directives (e.g. `public, max-age=3600`) X-Image-Source: schema: type: string description: Relative path of the source image that was served content: image/jpeg: schema: type: string format: binary image/png: schema: type: string format: binary '404': description: | No images available, folder not configured, or invalid parameters. /api/images/random/{width}/{height}/: get: operationId: getRandomImageSized tags: [Images] summary: Get a random image at specific dimensions description: | Returns a randomly selected image resized to `{width}×{height}` pixels. Minimum 300 px and maximum 4096 px for each dimension. **Cache busting:** Add a `?v={timestamp}` query parameter to prevent browsers from caching repeated calls (the server ignores this parameter). **Use in Markdown / posts:** ``` ![Random image](http://your-domain/api/images/random/300/450/?fit=crop&v=1234567890) ``` parameters: - in: path name: width required: true schema: type: integer minimum: 300 maximum: 4096 description: Target width in pixels example: 300 - in: path name: height required: true schema: type: integer minimum: 300 maximum: 4096 description: Target height in pixels example: 450 - in: query name: fit schema: type: string enum: [clip, crop, fill, scale] default: scale description: | How to fit the image into the requested dimensions. - `scale` — scale to fit, maintaining aspect ratio (may add letterbox) - `crop` — crop to exact dimensions - `clip` — clip to fit, no upscaling - `fill` — fill exact dimensions, may distort - in: query name: v schema: type: integer description: Cache-busting value (e.g. Unix timestamp). Ignored by the server. example: 1774152070 responses: '200': description: Image file (JPEG or PNG) at the requested dimensions headers: Cache-Control: schema: type: string X-Image-Source: schema: type: string description: Relative path of the source image that was served content: image/jpeg: schema: type: string format: binary image/png: schema: type: string format: binary '404': description: | No images available, folder not configured, invalid dimensions, or invalid fit mode. /import/images/{image_url}: get: operationId: importExternalImage tags: [Files] summary: Import and cache an external image description: | Imports an external image by URL and saves it to the local file store. The `image_url` path parameter is the image URL **without** the `https://` scheme prefix (e.g. `example.com/path/to/image.jpg`). The server always tries `https://` first. **Worker mode** — this endpoint is non-blocking: - On the **first request** for a given URL the server creates a `FileUpload` record (marked `is_public: true`) and kicks off a background thread to download and save the file. The response immediately redirects (HTTP 302) to the original `https://` source URL so the image is visible right away. - On **subsequent requests**, once the background download has completed, the response redirects to the locally-saved public file URL (`/public/files/{uuid}-{filename}`) so the original host is no longer needed. **Idempotent** — the same external URL always resolves to the same `FileUpload` record; the file is only downloaded once. **Use in Markdown / posts:** ``` ![Alt text](http://your-domain/import/images/example.com/path/to/image.jpg) ``` parameters: - in: path name: image_url required: true schema: type: string description: | External image URL without the scheme prefix. Example: `graziamagazine.com/wp-content/uploads/2024/12/Elle-Fanning-Pigtails-scaled.jpg` example: graziamagazine.com/wp-content/uploads/2024/12/Elle-Fanning-Pigtails-scaled.jpg responses: '302': description: | Redirect to either the original source URL (while background download is in progress) or the locally-saved public file URL (once download has completed). headers: Location: schema: type: string description: URL to redirect to (original source or local `/public/files/…`) components: schemas: Page: type: object properties: id: type: integer url: type: string title: type: string summary: type: string content: type: string created_at: type: string format: date-time updated_at: type: string format: date-time PageCreate: type: object required: - url - title properties: url: type: string title: type: string summary: type: string content: type: string PageList: type: object properties: count: type: integer next: type: string nullable: true previous: type: string nullable: true results: type: array items: $ref: '#/components/schemas/Page' Post: type: object properties: id: type: integer title: type: string content: type: string tag_details: type: array items: $ref: '#/components/schemas/Tag' created_at: type: string format: date-time updated_at: type: string format: date-time PostCreate: type: object required: - title - content properties: title: type: string content: type: string tags: type: array items: type: string description: List of tag slugs to associate with the post PostList: type: object properties: count: type: integer next: type: string nullable: true previous: type: string nullable: true results: type: array items: $ref: '#/components/schemas/Post' Tag: type: object properties: id: type: integer name: type: string slug: type: string description: type: string Collection: type: object properties: id: type: string format: uuid name: type: string description: type: string image_count: type: integer created_at: type: string format: date-time CollectionCreate: type: object required: - name properties: name: type: string description: type: string CollectionList: type: object properties: count: type: integer next: type: string nullable: true previous: type: string nullable: true results: type: array items: $ref: '#/components/schemas/Collection' CollectionResponse: type: object properties: status: type: string enum: [success] message: type: string data: $ref: '#/components/schemas/Collection' Image: type: object properties: id: type: string format: uuid collection: type: string format: uuid title: type: string description: type: string content_type: type: string size: type: integer created_at: type: string format: date-time updated_at: type: string format: date-time url: type: string FileUpload: type: object properties: id: type: string format: uuid readOnly: true name: type: string description: Original filename mime_type: type: string readOnly: true size: type: integer readOnly: true description: File size in bytes formatted_size: type: string readOnly: true description: Human-readable size (e.g. "1.4 MB") is_public: type: boolean public_token: type: string nullable: true readOnly: true public_url: type: string nullable: true readOnly: true example: /public/files// expires_at: type: string format: date-time nullable: true is_expired: type: boolean readOnly: true download_count: type: integer readOnly: true source_url: type: string format: uri nullable: true description: | Original external URL this file was imported from via `/import/images/…`. `null` for files uploaded directly. example: https://example.com/path/to/image.jpg created_at: type: string format: date-time readOnly: true updated_at: type: string format: date-time readOnly: true FileList: type: object properties: count: type: integer next: type: string nullable: true previous: type: string nullable: true results: type: array items: $ref: '#/components/schemas/FileUpload' SuccessResponse: type: object properties: status: type: string enum: [success] message: type: string TagSummary: type: object properties: id: type: integer name: type: string slug: type: string Link: type: object properties: id: type: integer readOnly: true alias: type: string description: Unique slug used as the short-link identifier (e.g. /gh → github.com). original_url: type: string description: Target URL. May contain template parameters like `{param,default=value}`. description: type: string nullable: true link_type: type: string enum: [LINK, CUSTOM] description: LINK redirects to original_url; CUSTOM renders Markdown content. click_count: type: integer readOnly: true description: Number of times the short link has been visited. tags: type: array readOnly: true items: $ref: '#/components/schemas/TagSummary' created_at: type: string format: date-time readOnly: true updated_at: type: string format: date-time readOnly: true LinkList: type: object properties: count: type: integer next: type: string nullable: true previous: type: string nullable: true results: type: array items: $ref: '#/components/schemas/Link'