openapi: 3.1.0 info: title: Dynjandi API version: 0.2.1 summary: Upload images to Dynjandi, read a stored file back, and address variants by URL. license: name: MIT identifier: MIT description: | Dynjandi is an image CDN at `https://cdn.dynjandi.dev`. This document describes the operations a project's public key authorises: `POST /upload` and `GET /files/{id}`. Variants of an uploaded image are addressed by URL, not by an API call; the URL grammar is described below. The spec is versioned on its own (semantic versioning of this document, see `info.version`). It covers only operations the public key authorises; the management API is not part of it until it has key-based authentication. ## Uploads and file reads are server-side only The service sends no CORS headers, and answers a preflight `OPTIONS` request with `405`. Both operations send `X-Public-Key`, which makes a cross-origin browser request preflighted, so the preflight fails and neither `POST /upload` nor `GET /files/{id}` is sent. Call both from a server or another non-browser runtime. Variants and originals load normally in ``, CSS and other non-CORS contexts, but GET responses carry no CORS headers either, so a cross-origin `fetch()` from a browser cannot read their bodies. ## Authentication `POST /upload` and `GET /files/{id}` take a project's public key in the `X-Public-Key` header. The public key is a publishable identifier for a project, not a secret, but it still selects the project that receives an upload or that a file is read from, and is checked on every request: a missing key is `401`, an unknown key is `401`. ## Relative URLs The `url` in a successful upload response is relative to the service origin (`/`). Clients resolve it against the origin they sent the request to (the server URL below by default) to get the image's address. ## Errors Every error response has the same body, `{ "error": "" }`, and carries `Cache-Control: no-store`. ## Variant URL grammar An uploaded image is served at `/`. A variant is addressed by appending a chain of operations to the file id. Each operation is written as `-///` and operations apply in the order given: ``` //-/resize/800x600/-/format/webp/-/quality/80/ ``` This is a description of what the service accepts, not an operation per route. A variant URL (`//-/...`) whose operations do not parse is answered with `400` and a message naming the problem (for example `Unknown operation: `). The operations are: - **`resize/x/`** scales the image. Either dimension may be left out (`800x`, `x600`) but not both. Each given dimension is an integer from 1 to 10000. With one dimension the aspect ratio is kept. With both, the image is scaled to cover the box and then centre-cropped to exactly that size: it is neither stretched nor fitted inside, so `resize/100x100/` of a 400x200 source shows the middle 200x200 of it. The service does not limit output to the source's size: `resize` enlarges a smaller source, so `resize/800x/` of a 400x200 source is 800x400. - **`format//`** sets the output format: `jpeg`, `png`, `webp` or `avif`. `jpg` is accepted as an alias for `jpeg`. - **`quality//`** sets output quality, an integer from 1 to 100, or one of the named levels `lightest` (55), `lighter` (70), `normal` (80), `smart` (80), `better` (88), `best` (95). - **`crop/x//`** produces an image of exactly that size. The image is first scaled to cover the box, as `resize/x/` does, and `` then chooses which part of the scaled image is kept. It does not extract a window at the source's native scale, and it enlarges a source smaller than the box: `crop/800x800/center/` of a 400x200 source is 800x800. Both dimensions are required integers from 1 to 10000. `` is one of: - a compass position: `center`, `north`, `south`, `east`, `west`, `northeast`, `northwest`, `southeast`, `southwest`; - a content-aware position: `attention` or `entropy`; - a face-detection chain, `face,`, where `` is any position above. The crop centres on a detected face and falls back to the named position when none is found. `face` alone means `face,center`, and `smart` is an alias for `face,center`; - `focal/x/`, the focal point as two decimals from 0 to 1 inclusive, for example `crop/1280x400/focal/0.42x0.18/`. `0,0` is the top left of the image. - **`scale_crop/x/`** is an Uploadcare-compatible alias of `crop`. The position is optional and defaults to `center`; `focal/x/` is accepted as for `crop`. Empty segments and lone `-` segments between operations are ignored, as is one trailing filename such as `photo.jpg`. `//-/` with nothing after it is answered with `400`. Examples: - `//-/resize/800x600/` - `//-/format/webp/` - `//-/quality/80/` - `//-/crop/400x400/center/` - `//-/crop/1280x400/focal/0.42x0.18/` - `//-/crop/600x600/face,attention/` servers: - url: https://cdn.dynjandi.dev description: Production tags: - name: Upload description: Adding an image to a project. - name: Files description: Reading back a file already stored in a project. paths: /upload: post: operationId: uploadFile tags: - Upload summary: Upload an image description: | Stores the file in the project identified by the public key and returns its id. The service stores any non-empty file; it does not check the file's type. The request is `multipart/form-data` with a `file` part and, optionally, a focal point given as both `focalX` and `focalY`. Giving only one of them is an error, as is an empty value. The file must be non-empty, and must fit the project's maximum file size and remaining storage quota. Uploads are server-side only; see the API description on CORS. security: - publicKey: [] requestBody: required: true content: multipart/form-data: schema: $ref: "#/components/schemas/UploadRequest" responses: "201": description: The file was stored. headers: Cache-Control: $ref: "#/components/headers/CacheControlNoStore" content: application/json: schema: $ref: "#/components/schemas/UploadResponse" example: file: 00000000-0000-4000-8000-000000000000 id: 00000000-0000-4000-8000-000000000000 url: /00000000-0000-4000-8000-000000000000 focalX: 0.42 focalY: 0.18 "400": description: | The request is malformed. The `error` message is one of: `Expected multipart/form-data` (wrong content type), `Invalid multipart body` (the body cannot be parsed), `No file provided` (`file` is missing, not a file, or empty), `Both focalX and focalY are required` (only one given, or an empty value), `Invalid focal coordinates` (not a number, or outside 0 to 1). headers: Cache-Control: $ref: "#/components/headers/CacheControlNoStore" content: application/json: schema: $ref: "#/components/schemas/Error" example: error: No file provided "401": description: | The public key is missing (`Missing X-Public-Key`) or unknown (`Invalid public key`). headers: Cache-Control: $ref: "#/components/headers/CacheControlNoStore" content: application/json: schema: $ref: "#/components/schemas/Error" example: error: Invalid public key "413": description: | The file is too large. The `error` message is `File exceeds maximum allowed size` (over the project's per-file limit) or `File exceeds project storage quota`. headers: Cache-Control: $ref: "#/components/headers/CacheControlNoStore" content: application/json: schema: $ref: "#/components/schemas/Error" example: error: File exceeds maximum allowed size "500": description: An unexpected error. The `error` message is always `Internal server error`. headers: Cache-Control: $ref: "#/components/headers/CacheControlNoStore" content: application/json: schema: $ref: "#/components/schemas/Error" example: error: Internal server error /files/{id}: get: operationId: getFile tags: - Files summary: Read a stored file description: | Returns the record of a file stored in the project identified by the public key, including the focal point saved at upload or later in the dashboard. Pass the point to a variant URL (see the `focal` crop position) to crop on it: variants are cached by URL, so the service does not apply it for you. The response is `no-store`; keep the record rather than reading it per image. A file in another project, an unknown id and an id that is not a UUID are all `404`. Reads are server-side only; see the API description on CORS. security: - publicKey: [] parameters: - name: id in: path required: true description: The file id, as returned by `POST /upload`. schema: type: string format: uuid responses: "200": description: The file record. headers: Cache-Control: $ref: "#/components/headers/CacheControlNoStore" content: application/json: schema: $ref: "#/components/schemas/FileRecord" example: id: 00000000-0000-4000-8000-000000000000 contentType: image/png bytes: 70 originalFilename: photo.png source: upload focalX: 0.42 focalY: 0.18 createdAt: "2026-10-04T15:31:34.000Z" "401": description: | The public key is missing (`Missing X-Public-Key`) or unknown (`Invalid public key`). headers: Cache-Control: $ref: "#/components/headers/CacheControlNoStore" content: application/json: schema: $ref: "#/components/schemas/Error" example: error: Invalid public key "404": description: | The file is not found (`File not found`): no such id, an id that is not a UUID, or a file outside the key's project. headers: Cache-Control: $ref: "#/components/headers/CacheControlNoStore" content: application/json: schema: $ref: "#/components/schemas/Error" example: error: File not found components: securitySchemes: publicKey: type: apiKey in: header name: X-Public-Key description: | A project's public key. It is a publishable identifier, not a secret, and identifies the project that receives an upload or holds the file being read. It is an opaque string, shown on the project's page in the dashboard. headers: CacheControlNoStore: description: Always `no-store`; responses from this API must not be cached. required: true schema: type: string const: no-store schemas: UploadRequest: type: object required: - file properties: file: type: string format: binary description: The file to store, normally an image. Must be non-empty; its type is not checked. focalX: type: number minimum: 0 maximum: 1 description: | Horizontal focal point, 0 (left) to 1 (right), inclusive. Send together with `focalY` or not at all. Multipart values are text, so send it as a decimal such as `0.42`. focalY: type: number minimum: 0 maximum: 1 description: | Vertical focal point, 0 (top) to 1 (bottom), inclusive. Send together with `focalX` or not at all. UploadResponse: type: object required: - file - id - url - focalX - focalY properties: file: type: string format: uuid description: The file id. Always the same value as `id`. id: type: string format: uuid description: The file id, as used in variant URLs. url: type: string pattern: "^/" description: | The image's path, `/`, relative to the service origin. Resolve it against the origin the request was sent to. focalX: type: - number - "null" minimum: 0 maximum: 1 description: The stored horizontal focal point, or `null` when none was given. focalY: type: - number - "null" minimum: 0 maximum: 1 description: The stored vertical focal point, or `null` when none was given. FileRecord: type: object required: - id - contentType - bytes - originalFilename - source - focalX - focalY - createdAt properties: id: type: string format: uuid description: The file id, as used in variant URLs. contentType: type: string description: The stored media type, for example `image/png`. bytes: type: integer minimum: 0 description: The size of the stored file in bytes. originalFilename: type: - string - "null" description: The filename sent with the upload, or `null` when there was none. source: type: string description: | How the file got into the project, for example `upload`. The set of values is not documented. focalX: type: - number - "null" minimum: 0 maximum: 1 description: The stored horizontal focal point, or `null` when none is stored. focalY: type: - number - "null" minimum: 0 maximum: 1 description: The stored vertical focal point, or `null` when none is stored. createdAt: type: string format: date-time description: When the file was stored, an ISO 8601 UTC timestamp. Error: type: object required: - error properties: error: type: string description: A human-readable message. It is stable enough to show, not to branch on; branch on the status code.