{
  "openapi": "3.1.0",
  "info": {
    "title": "ZawCloud Images delivery API",
    "version": "1.0.0",
    "summary": "Resize, crop and re-encode images from your own origin, at the edge.",
    "description": "ZawCloud Images is an image CDN in front of images you already host. There is no upload endpoint: a delivery URL names a path at your account's origin, and an edge node fetches the original, transforms it as the query string says, caches the result and serves it.\n\n```\nhttps://a3f9c21b.zwcld.net/v1/products/shoe.jpg?w=800&f=auto\n```\n\n## Authenticating\n\nEvery request is authenticated in exactly one of these ways, checked in this order:\n\n1. **A signed URL.** `exp`, `kid` and `sig` in the query string (and optionally `cip`). The right choice for anything a browser fetches, because the URL carries no API key. Signing secrets are shown on the console's *Signed URLs* tab. A request carrying `sig` is never retried as an API-key request when the signature fails.\n2. **An API key**, as `Authorization: Bearer <key>` (server to server) or `?key=<key>` (an `<img>` cannot send headers). Keys look like `zaw_live_` followed by 32 hex characters.\n3. **The hostname.** A request to your platform hostname or to a verified custom domain identifies the account by itself, so `<img src>` needs no credential at all.\n\nA key's allowed domains, and the account's Bot & Hotlink Shield, are applied after authentication. Both read `Origin`, `Referer` and `User-Agent`, which the client chooses: they stop other sites' pages and self-identifying crawlers, and are not access control. Use signed URLs for that.\n\n### Signing a URL\n\n```\nstring    = \"ZAWCLOUD-URL-V1\\n\" + objectPath + \"\\n\" + canonicalQuery\nsig       = base64url(HMAC-SHA256(signingSecret, string))\n```\n\n`canonicalQuery` is every query parameter except `sig` (so `exp`, `kid` and `cip` are covered), percent-decoded, sorted by name then value (ordinal), percent-encoded again and joined with `&` and `=`. The transformations are inside the signature, so a signed URL cannot be edited into another size or format.\n\n## Errors\n\nA refusal is JSON, `{\"error\": \"...\"}`, with a sentence meant for a person. Quota and AI-credit refusals add the figures behind them. The one exception is a Bot & Hotlink Shield refusal on an account that chose the placeholder response: a 403 whose body is a small grey SVG, so an embedded image does not show as broken.\n\n## Caching\n\nRenditions are cached at the edge and sent with `Cache-Control: public, max-age=…, immutable` and a strong `ETag`. Send the `ETag` back as `If-None-Match` for a `304`. When the original changes, purge it (`POST /v1/purge`, or the console's *Cache* tab, which reaches every node).\n\n## Limits\n\n- Still images only. Animation and video are not transcoded.\n- Width and height are capped at 5000 px by default; a node's operator may set another cap.\n- An account may restrict the widths and heights it serves, or require every request to use a named preset. Both are set in the console, and a request outside them is a `400`.\n- AVIF output, and the Bot & Hotlink Shield, depend on the account's plan.",
    "contact": {
      "name": "ZawCloud support",
      "url": "/Home/Contact"
    },
    "termsOfService": "/Legal/Terms"
  },
  "externalDocs": {
    "description": "Framework integrations: @zawcloud/images for Next.js, Nuxt and Astro",
    "url": "/integrations"
  },
  "servers": [
    {
      "url": "https://{hostname}",
      "description": "Your delivery hostname: the platform hostname shown in the console, or a verified custom domain.",
      "variables": {
        "hostname": {
          "default": "a3f9c21b.zwcld.net",
          "description": "For example a3f9c21b.zwcld.net, or img.yourbrand.com once verified."
        }
      }
    }
  ],
  "tags": [
    {
      "name": "Delivery",
      "description": "Transformed images, and JSON describing them."
    },
    {
      "name": "Cache",
      "description": "Evicting cached renditions when an original changes."
    }
  ],
  "security": [
    { "bearerKey": [] },
    { "queryKey": [] },
    {}
  ],
  "paths": {
    "/v1/{path}": {
      "get": {
        "tags": ["Delivery"],
        "operationId": "getImage",
        "summary": "Deliver a transformed image",
        "description": "Fetches `{path}` from the account's origin, applies the query parameters and returns the result. Parameters are applied in a fixed order whatever order they are written in: `crop`, `rotate`, `flip`, `trim`, resize and fit, colour adjustments, effects, overlay, watermark, border, corners.\n\nOn a **verified custom domain** the `/v1` prefix is optional: `https://img.yourbrand.com/products/shoe.jpg` is the same request.\n\n`blurhash`, `thumbhash` and `info` return JSON (or CSS) describing the rendition instead of the image. Every other parameter still applies, so the document describes exactly what the same URL without them delivers.",
        "parameters": [
          { "$ref": "#/components/parameters/path" },
          { "$ref": "#/components/parameters/w" },
          { "$ref": "#/components/parameters/h" },
          { "$ref": "#/components/parameters/f" },
          { "$ref": "#/components/parameters/q" },
          { "$ref": "#/components/parameters/dpr" },
          { "$ref": "#/components/parameters/fit" },
          { "$ref": "#/components/parameters/bg" },
          { "$ref": "#/components/parameters/fp" },
          { "$ref": "#/components/parameters/trim" },
          { "$ref": "#/components/parameters/trimtol" },
          { "$ref": "#/components/parameters/crop" },
          { "$ref": "#/components/parameters/rotate" },
          { "$ref": "#/components/parameters/flip" },
          { "$ref": "#/components/parameters/bri" },
          { "$ref": "#/components/parameters/con" },
          { "$ref": "#/components/parameters/sat" },
          { "$ref": "#/components/parameters/gam" },
          { "$ref": "#/components/parameters/blur" },
          { "$ref": "#/components/parameters/grayscale" },
          { "$ref": "#/components/parameters/sharpen" },
          { "$ref": "#/components/parameters/radius" },
          { "$ref": "#/components/parameters/mask" },
          { "$ref": "#/components/parameters/border" },
          { "$ref": "#/components/parameters/strip" },
          { "$ref": "#/components/parameters/watermark" },
          { "$ref": "#/components/parameters/wmpos" },
          { "$ref": "#/components/parameters/wmopacity" },
          { "$ref": "#/components/parameters/wmsize" },
          { "$ref": "#/components/parameters/overlay" },
          { "$ref": "#/components/parameters/opos" },
          { "$ref": "#/components/parameters/oopacity" },
          { "$ref": "#/components/parameters/osize" },
          { "$ref": "#/components/parameters/preset" },
          { "$ref": "#/components/parameters/blurhash" },
          { "$ref": "#/components/parameters/bhx" },
          { "$ref": "#/components/parameters/bhy" },
          { "$ref": "#/components/parameters/thumbhash" },
          { "$ref": "#/components/parameters/info" },
          { "$ref": "#/components/parameters/class" },
          { "$ref": "#/components/parameters/ai_crop" },
          { "$ref": "#/components/parameters/ai_remove_bg" },
          { "$ref": "#/components/parameters/ai_upscale" },
          { "$ref": "#/components/parameters/ai_enhance" },
          { "$ref": "#/components/parameters/exp" },
          { "$ref": "#/components/parameters/kid" },
          { "$ref": "#/components/parameters/cip" },
          { "$ref": "#/components/parameters/sig" },
          { "$ref": "#/components/parameters/Accept" },
          { "$ref": "#/components/parameters/IfNoneMatch" },
          { "$ref": "#/components/parameters/SecChDpr" },
          { "$ref": "#/components/parameters/SecChWidth" },
          { "$ref": "#/components/parameters/SecChViewportWidth" },
          { "$ref": "#/components/parameters/SaveData" }
        ],
        "responses": {
          "200": {
            "description": "The rendition, or with `blurhash`, `thumbhash` or `info` a document describing it.",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
              "Vary": { "$ref": "#/components/headers/Vary" }
            },
            "content": {
              "image/avif": { "schema": { "type": "string", "contentMediaType": "image/avif" } },
              "image/webp": { "schema": { "type": "string", "contentMediaType": "image/webp" } },
              "image/jpeg": { "schema": { "type": "string", "contentMediaType": "image/jpeg" } },
              "image/png": { "schema": { "type": "string", "contentMediaType": "image/png" } },
              "image/gif": { "schema": { "type": "string", "contentMediaType": "image/gif" } },
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/BlurHash" },
                    { "$ref": "#/components/schemas/ThumbHash" },
                    { "$ref": "#/components/schemas/ImageInfo" }
                  ]
                },
                "examples": {
                  "blurhash": {
                    "summary": "?blurhash=true",
                    "value": { "blurhash": "LWN14Q.8?It8~qV@IUofoeRjWBof", "width": 800, "height": 533, "averageColour": "#C8C7CC", "components": { "x": 4, "y": 3 } }
                  },
                  "thumbhash": {
                    "summary": "?thumbhash=true",
                    "value": { "thumbhash": "1QcSHQRnh493V4dIh4eXh1h4kJUI", "width": 400, "height": 267, "averageColour": "#7A8C9B", "hasAlpha": false }
                  }
                }
              },
              "text/css": {
                "schema": { "type": "string" },
                "example": ".hero{aspect-ratio:800/533;background:#C8C2BC url(\"data:image/webp;base64,...\") center/cover no-repeat}"
              }
            }
          },
          "304": {
            "description": "The `If-None-Match` ETag still matches. No body.",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
              "Vary": { "$ref": "#/components/headers/Vary" }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": {
            "description": "The account has used its plan's monthly allowance, or the request uses an AI feature the account has no credits for.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/QuotaExceeded" },
                    { "$ref": "#/components/schemas/AiCredits" }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Refused before anything was fetched: a signed URL that expired, does not verify, lives too long or was bound to another client; a key used from a site outside its allowed domains; the Bot & Hotlink Shield; or an account whose delivery is suspended. A signed URL has to be reissued; no header fixes it.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } },
              "image/svg+xml": {
                "schema": {
                  "type": "string",
                  "description": "The Bot & Hotlink Shield's placeholder, when the account chose it. Sent with `Cache-Control: no-store`."
                }
              }
            }
          },
          "404": {
            "description": "The origin has no such object, the account has no preset of that name, or no account is served on this hostname.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "413": {
            "description": "The original is larger than a node will fetch.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "415": {
            "description": "The original could not be decoded or transformed.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "The account is sending more requests per second to this node than its limit.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait.",
                "schema": { "type": "integer" }
              }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "451": {
            "description": "The image, or its overlay, was removed following an abuse report.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "501": {
            "description": "The AI feature asked for needs a model this node does not have.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "502": {
            "description": "The origin could not be reached, refused the connection, or answered with an error.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "503": { "$ref": "#/components/responses/Unavailable" },
          "504": {
            "description": "The origin did not respond in time.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/v1/purge": {
      "post": {
        "tags": ["Cache"],
        "operationId": "purge",
        "summary": "Evict cached renditions",
        "description": "Drops every cached transformation of an object, or of every object under a prefix, so the next request fetches the original again.\n\n**This clears the node that answers, plus the store the whole fleet shares.** Other nodes keep their in-memory copies until they expire. To clear every node at once, use the console's *Cache* tab, which sends the purge to each of them and records how many answered.\n\nPurging is by object, not by variant: a query string on `target` is ignored, and every size and format of the object goes. `target` may be the object path, the `/v1/` path, or a full delivery URL; the host in a full URL is ignored.",
        "security": [
          { "bearerKey": [] },
          { "queryKey": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PurgeRequest" },
              "examples": {
                "object": { "summary": "One object, every variant", "value": { "target": "demo/hero.jpg" } },
                "prefix": { "summary": "Everything under a prefix", "value": { "target": "products/*" } },
                "url": { "summary": "A delivery URL as served", "value": { "target": "https://a3f9c21b.zwcld.net/v1/demo/hero.jpg?w=1200&f=avif" } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was evicted.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PurgeResult" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "`scope: all` empties a whole node, and is reserved for the operator.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key, `zaw_live_` + 32 hex characters. For server-to-server calls. Requests authenticated this way are not subject to the Bot & Hotlink Shield."
      },
      "queryKey": {
        "type": "apiKey",
        "in": "query",
        "name": "key",
        "description": "The same key in the query string, for an `<img>` that cannot send headers. It ends up in browser history and logs and never expires; prefer a signed URL for anything a browser loads."
      }
    },
    "parameters": {
      "path": {
        "name": "path",
        "in": "path",
        "required": true,
        "description": "The object's path at the account's origin, and may contain slashes: `products/shoe.jpg` fetches `{origin}/products/shoe.jpg`.",
        "schema": { "type": "string" },
        "example": "products/shoe.jpg"
      },
      "w": {
        "name": "w",
        "in": "query",
        "description": "Output width in pixels. Given alone, the height follows the aspect ratio. `auto` takes the width from client hints (`Sec-CH-Width`, then viewport width times DPR), rounded up to a multiple of 100, falling back to 1280, or to the number after the colon as in `auto:800`. `w=auto` cannot be combined with `h` or `dpr`.",
        "schema": {
          "oneOf": [
            { "type": "integer", "minimum": 1, "maximum": 5000 },
            { "type": "string", "pattern": "^auto(:[0-9]+)?$" }
          ]
        },
        "example": 800
      },
      "h": {
        "name": "h",
        "in": "query",
        "description": "Output height in pixels. Given alone, the width follows the aspect ratio.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 5000 }
      },
      "f": {
        "name": "f",
        "in": "query",
        "description": "Output format. `auto` picks AVIF, then WebP, from the browser's `Accept` header, otherwise keeps the origin's format; responses then carry `Vary: Accept`. `jpg` is accepted as `jpeg`. AVIF depends on the plan: where it is not included, `auto` stops at WebP and `f=avif` is refused with 400.",
        "schema": { "type": "string", "enum": ["auto", "avif", "webp", "jpeg", "jpg", "png", "gif"], "default": "auto" }
      },
      "q": {
        "name": "q",
        "in": "query",
        "description": "Encoding quality. `auto` searches for the lowest quality that still measures as visually the same as the rendition (by DSSIM). The search costs several encodes on the first request and is cached like any other rendition.",
        "schema": {
          "oneOf": [
            { "type": "integer", "minimum": 1, "maximum": 100, "default": 80 },
            { "type": "string", "const": "auto" }
          ]
        }
      },
      "dpr": {
        "name": "dpr",
        "in": "query",
        "description": "Pixel density: multiplies `w` and `h`, held to the size cap. `auto` reads `Sec-CH-DPR` (or `DPR`) and rounds it up to 1, 1.5, 2 or 3; with no hint it is 1. With `Save-Data: on`, `auto` means 1x and quality at most 50 unless `q` is given.",
        "schema": {
          "oneOf": [
            { "type": "number", "minimum": 1, "maximum": 4, "default": 1 },
            { "type": "string", "const": "auto" }
          ]
        }
      },
      "fit": {
        "name": "fit",
        "in": "query",
        "description": "How the image fills a `w` × `h` box. `cover` fills it and crops the overflow (`crop` is a synonym). `contain` fits inside it and never upscales. `fill` stretches. `pad` is `contain` plus a border of `bg` to exactly `w` × `h`.",
        "schema": { "type": "string", "enum": ["cover", "crop", "contain", "fill", "pad"], "default": "cover" }
      },
      "bg": {
        "name": "bg",
        "in": "query",
        "description": "Background colour, as hex `rgb`, `rgba`, `rrggbb` or `rrggbbaa` (a leading `#` is optional, written `%23`). Fills `fit=pad`'s border and the background transparency is flattened onto for JPEG. Default transparent, white in JPEG.",
        "schema": { "type": "string", "pattern": "^#?([0-9A-Fa-f]{3,4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$" },
        "example": "f5f5f5"
      },
      "fp": {
        "name": "fp",
        "in": "query",
        "description": "Focal point, `x,y`, each 0 to 1, on the upright original: where a `cover` crop with both `w` and `h` is centred. Followed through `rotate`, `flip` and `trim`. Cannot be combined with `ai_crop`.",
        "schema": { "type": "string", "pattern": "^[0-9.]+,[0-9.]+$" },
        "example": "0.3,0.4"
      },
      "trim": {
        "name": "trim",
        "in": "query",
        "description": "Cuts away a border of near-uniform colour before resizing. `1` is accepted as `true`.",
        "schema": { "type": "boolean", "default": false }
      },
      "trimtol": {
        "name": "trimtol",
        "in": "query",
        "description": "With `trim`: how far, in percent, a pixel may differ from the border colour and still be trimmed.",
        "schema": { "type": "integer", "minimum": 0, "maximum": 50, "default": 10 }
      },
      "crop": {
        "name": "crop",
        "in": "query",
        "description": "A rectangle `x,y,w,h` in pixels of the upright original, cut out before anything else. Clamped to the image; a rectangle entirely outside it is a 400. Not the same as `fit=crop`.",
        "schema": { "type": "string", "pattern": "^[0-9]+,[0-9]+,[0-9]+,[0-9]+$" },
        "example": "100,50,800,600"
      },
      "rotate": {
        "name": "rotate",
        "in": "query",
        "description": "Rotation in degrees, a multiple of 90, applied before resizing.",
        "schema": { "type": "integer", "multipleOf": 90, "default": 0 }
      },
      "flip": {
        "name": "flip",
        "in": "query",
        "description": "Mirror left-right (`h`), top-bottom (`v`) or both. Applied after `rotate`.",
        "schema": { "type": "string", "enum": ["h", "v", "hv"] }
      },
      "bri": {
        "name": "bri",
        "in": "query",
        "description": "Brightness.",
        "schema": { "type": "integer", "minimum": -100, "maximum": 100, "default": 0 }
      },
      "con": {
        "name": "con",
        "in": "query",
        "description": "Contrast.",
        "schema": { "type": "integer", "minimum": -100, "maximum": 100, "default": 0 }
      },
      "sat": {
        "name": "sat",
        "in": "query",
        "description": "Saturation. `-100` is grey.",
        "schema": { "type": "integer", "minimum": -100, "maximum": 100, "default": 0 }
      },
      "gam": {
        "name": "gam",
        "in": "query",
        "description": "Gamma. Above 1 lightens the midtones.",
        "schema": { "type": "number", "minimum": 0.1, "maximum": 10, "default": 1 }
      },
      "blur": {
        "name": "blur",
        "in": "query",
        "description": "Blur strength.",
        "schema": { "type": "integer", "minimum": 0, "maximum": 100, "default": 0 }
      },
      "grayscale": {
        "name": "grayscale",
        "in": "query",
        "description": "Converts to grey. `1` is accepted as `true`.",
        "schema": { "type": "boolean", "default": false }
      },
      "sharpen": {
        "name": "sharpen",
        "in": "query",
        "description": "Sharpens the output. `1` is accepted as `true`.",
        "schema": { "type": "boolean", "default": false }
      },
      "radius": {
        "name": "radius",
        "in": "query",
        "description": "Rounded corners, in pixels. A radius past half the short edge makes a pill. Corners become transparent; `f=auto` keeps that (PNG for a browser with neither AVIF nor WebP), and `f=jpeg` fills them with `bg`. Not with `mask`.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 2500 }
      },
      "mask": {
        "name": "mask",
        "in": "query",
        "description": "Cuts the image to the ellipse inscribed in it: a circle when `w` and `h` match. Not with `radius`.",
        "schema": { "type": "string", "enum": ["circle"] }
      },
      "border": {
        "name": "border",
        "in": "query",
        "description": "A solid border outside the image, `width[,colour]`: width 1 to 100 pixels, colour as for `bg`, default black. The image grows by twice the width. Rounded when combined with `radius`.",
        "schema": { "type": "string", "pattern": "^[0-9]{1,3}(,#?[0-9A-Fa-f]{3,8})?$" },
        "example": "4,ffffff"
      },
      "strip": {
        "name": "strip",
        "in": "query",
        "description": "Removes EXIF and other metadata, including location, after converting wide-gamut images to sRGB so their colours survive. **On by default**; `strip=false` keeps the metadata and the colour profile.",
        "schema": { "type": "boolean", "default": true }
      },
      "watermark": {
        "name": "watermark",
        "in": "query",
        "description": "Text burned into the output.",
        "schema": { "type": "string", "maxLength": 100 }
      },
      "wmpos": {
        "name": "wmpos",
        "in": "query",
        "description": "Where the watermark sits.",
        "schema": { "$ref": "#/components/schemas/Position" }
      },
      "wmopacity": {
        "name": "wmopacity",
        "in": "query",
        "description": "Watermark opacity, in percent.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 60 }
      },
      "wmsize": {
        "name": "wmsize",
        "in": "query",
        "description": "Watermark font size, as a percentage of the output's shorter edge.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 25, "default": 5 }
      },
      "overlay": {
        "name": "overlay",
        "in": "query",
        "description": "Path of an image at the same origin to composite on top, such as a logo.",
        "schema": { "type": "string", "maxLength": 300 },
        "example": "brand/logo.png"
      },
      "opos": {
        "name": "opos",
        "in": "query",
        "description": "Where the overlay sits.",
        "schema": { "$ref": "#/components/schemas/Position" }
      },
      "oopacity": {
        "name": "oopacity",
        "in": "query",
        "description": "Overlay opacity, in percent.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 100 }
      },
      "osize": {
        "name": "osize",
        "in": "query",
        "description": "Overlay width, as a percentage of the output's shorter edge.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }
      },
      "preset": {
        "name": "preset",
        "in": "query",
        "description": "One of the account's named presets, defined in the console. It supplies defaults; any parameter written in the URL wins over it. An unknown name is a 404. An account can require every request to use one.",
        "schema": { "type": "string" },
        "example": "hero-hd"
      },
      "blurhash": {
        "name": "blurhash",
        "in": "query",
        "description": "Returns a [BlurHash](https://blurha.sh) placeholder of the rendition as JSON instead of the image. Not with `info` or `thumbhash`.",
        "schema": { "type": "boolean", "default": false }
      },
      "bhx": {
        "name": "bhx",
        "in": "query",
        "description": "BlurHash horizontal components. More means a longer string and more detail.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 9, "default": 4 }
      },
      "bhy": {
        "name": "bhy",
        "in": "query",
        "description": "BlurHash vertical components.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 9, "default": 3 }
      },
      "thumbhash": {
        "name": "thumbhash",
        "in": "query",
        "description": "Returns a [ThumbHash](https://evanw.github.io/thumbhash/) of the rendition as JSON: about 25 bytes, and keeps transparency and aspect ratio. Not with `info` or `blurhash`.",
        "schema": { "type": "boolean", "default": false }
      },
      "info": {
        "name": "info",
        "in": "query",
        "description": "`true` returns JSON with everything needed to reserve and paint the image's slot before it loads: size, colours, both hashes, a tiny WebP preview and facts about the original. `css` returns the same as one CSS rule (`text/css`) for the class named by `class`. Not with `blurhash` or `thumbhash`.",
        "schema": {
          "oneOf": [
            { "type": "boolean" },
            { "type": "string", "const": "css" }
          ]
        }
      },
      "class": {
        "name": "class",
        "in": "query",
        "description": "With `info=css`: the CSS class the rule is written for. Refused without `info=css`.",
        "schema": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_-]{0,63}$", "default": "zc-image" }
      },
      "ai_crop": {
        "name": "ai_crop",
        "in": "query",
        "description": "Crops around the detected subject, or around faces with `face`, instead of the centre. Needs the AI add-on, and a model on the node (501 otherwise). Not with `fp`.",
        "schema": { "type": "string", "enum": ["true", "face"] }
      },
      "ai_remove_bg": {
        "name": "ai_remove_bg",
        "in": "query",
        "description": "Cuts the background out into transparency. Needs the AI add-on and a model on the node. Refuses `f=jpeg`, which cannot carry transparency.",
        "schema": { "type": "string", "enum": ["true"] }
      },
      "ai_upscale": {
        "name": "ai_upscale",
        "in": "query",
        "description": "Super-resolution, by 2 or 4 times. Needs the AI add-on and a model on the node.",
        "schema": { "type": "string", "enum": ["2x", "4x"] }
      },
      "ai_enhance": {
        "name": "ai_enhance",
        "in": "query",
        "description": "Automatic tone and colour correction. Needs the AI add-on.",
        "schema": { "type": "string", "enum": ["true"] }
      },
      "exp": {
        "name": "exp",
        "in": "query",
        "description": "Signed URL: expiry, in Unix seconds. A node refuses a URL that expires further ahead than its maximum lifetime.",
        "schema": { "type": "integer" }
      },
      "kid": {
        "name": "kid",
        "in": "query",
        "description": "Signed URL: the id of the API key whose signing secret made `sig`. Shown on the console's Signed URLs tab.",
        "schema": { "type": "string" }
      },
      "cip": {
        "name": "cip",
        "in": "query",
        "description": "Signed URL, optional: binds the URL to one client address. IPv4 matches exactly; IPv6 matches on the /64.",
        "schema": { "type": "string" },
        "example": "198.51.100.7"
      },
      "sig": {
        "name": "sig",
        "in": "query",
        "description": "Signed URL: base64url HMAC-SHA256 over the canonical request. See *Signing a URL* above.",
        "schema": { "type": "string" }
      },
      "Accept": {
        "name": "Accept",
        "in": "header",
        "description": "Read by `f=auto` to choose AVIF or WebP.",
        "schema": { "type": "string" },
        "example": "image/avif,image/webp,*/*"
      },
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "description": "An `ETag` from an earlier response. A match is answered with 304 and no body.",
        "schema": { "type": "string" }
      },
      "SecChDpr": {
        "name": "Sec-CH-DPR",
        "in": "header",
        "description": "Client hint read by `dpr=auto` (and `w=auto`). `DPR` is accepted too. Browsers send it to the image host only when the page delegates it, for example with `<meta http-equiv=\"Delegate-CH\" content=\"sec-ch-dpr https://a3f9c21b.zwcld.net\">`. Chromium browsers send hints; Firefox and Safari do not.",
        "schema": { "type": "number" }
      },
      "SecChWidth": {
        "name": "Sec-CH-Width",
        "in": "header",
        "description": "Client hint read by `w=auto`: the image slot's width in device pixels, from its `sizes` attribute. `Width` is accepted too.",
        "schema": { "type": "integer" }
      },
      "SecChViewportWidth": {
        "name": "Sec-CH-Viewport-Width",
        "in": "header",
        "description": "Client hint read by `w=auto` when there is no slot width. `Viewport-Width` is accepted too.",
        "schema": { "type": "integer" }
      },
      "SaveData": {
        "name": "Save-Data",
        "in": "header",
        "description": "`on` lowers the density and quality of `dpr=auto` and `w=auto` URLs, unless the URL sets `q`. Ignored on URLs that fix their own size.",
        "schema": { "type": "string", "enum": ["on"] }
      }
    },
    "headers": {
      "ETag": {
        "description": "Strong entity tag over the delivered bytes.",
        "schema": { "type": "string" }
      },
      "CacheControl": {
        "description": "`public, max-age=…, immutable`.",
        "schema": { "type": "string" }
      },
      "Vary": {
        "description": "`Accept` for `f=auto`; the client-hint headers and `Save-Data` for `dpr=auto` and `w=auto`.",
        "schema": { "type": "string" }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "A parameter is out of range or malformed, two parameters conflict, or the request is outside the account's permitted sizes, formats or presets-only rule.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "Rotate must be a multiple of 90." }
          }
        }
      },
      "Unauthorized": {
        "description": "No credential, and the hostname does not identify an account.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "A valid API key is required. Pass it as 'Authorization: Bearer <key>', '?key=<key>', or use a signed URL." }
          }
        }
      },
      "Unavailable": {
        "description": "The node cannot check credentials or presets right now. Nothing is wrong with the request; retry shortly.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "This node cannot verify credentials right now. Try again shortly." }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "What went wrong, for a person to read." }
        }
      },
      "QuotaExceeded": {
        "type": "object",
        "required": ["error", "plan"],
        "properties": {
          "error": { "type": "string" },
          "plan": { "type": "string", "description": "The account's plan." },
          "deliveries": { "type": "integer", "description": "Images delivered this period." },
          "deliveryAllowance": { "type": "integer" },
          "deliveredKB": { "type": "integer", "description": "Kilobytes delivered this period." },
          "bandwidthAllowanceKB": { "type": "integer" },
          "resetsAt": { "type": "string", "format": "date-time", "description": "When the allowance starts again." }
        }
      },
      "AiCredits": {
        "type": "object",
        "required": ["error", "plan", "creditsRequired"],
        "description": "The account has no AI add-on, or has used this month's AI credits. The counts are present in the second case.",
        "properties": {
          "error": { "type": "string" },
          "plan": { "type": "string" },
          "creditsRequired": { "type": "integer" },
          "creditsUsed": { "type": "integer" },
          "creditsAllowance": { "type": "integer" },
          "resetsAt": { "type": "string", "format": "date-time" }
        }
      },
      "Position": {
        "type": "string",
        "enum": ["top-left", "top-right", "bottom-left", "bottom-right", "center"],
        "default": "bottom-right"
      },
      "BlurHash": {
        "type": "object",
        "required": ["blurhash", "width", "height", "averageColour", "components"],
        "properties": {
          "blurhash": { "type": "string" },
          "width": { "type": "integer", "description": "The rendition's width." },
          "height": { "type": "integer", "description": "The rendition's height." },
          "averageColour": { "type": "string", "description": "Hex `#RRGGBB`, from the hash, for clients with no decoder." },
          "components": {
            "type": "object",
            "required": ["x", "y"],
            "properties": {
              "x": { "type": "integer" },
              "y": { "type": "integer" }
            }
          }
        }
      },
      "ThumbHash": {
        "type": "object",
        "required": ["thumbhash", "width", "height", "averageColour", "hasAlpha"],
        "properties": {
          "thumbhash": { "type": "string", "description": "The hash bytes, standard base64." },
          "width": { "type": "integer" },
          "height": { "type": "integer" },
          "averageColour": { "type": "string" },
          "hasAlpha": { "type": "boolean" }
        }
      },
      "ImageInfo": {
        "type": "object",
        "required": ["width", "height", "aspectRatio", "dominantColour", "averageColour", "blurhash", "thumbhash", "preview", "origin"],
        "properties": {
          "width": { "type": "integer", "description": "The rendition's width: write it on the `<img>` to prevent layout shift." },
          "height": { "type": "integer" },
          "aspectRatio": { "type": "number" },
          "dominantColour": { "type": "string", "description": "The most common colour, ignoring transparency." },
          "averageColour": { "type": "string", "description": "The mean colour, from the BlurHash." },
          "blurhash": { "type": "string", "description": "As `blurhash=true` returns, with 4 × 3 components." },
          "thumbhash": { "type": "string", "description": "As `thumbhash=true` returns." },
          "preview": { "type": "string", "description": "The rendition at most 20 px on its long edge, as a WebP data URI." },
          "origin": {
            "type": "object",
            "description": "The stored original. The only EXIF field reported is the orientation.",
            "required": ["width", "height", "format", "bytes", "orientation", "hasAlpha"],
            "properties": {
              "width": { "type": "integer" },
              "height": { "type": "integer" },
              "format": { "type": "string" },
              "bytes": { "type": "integer" },
              "orientation": { "type": "integer", "minimum": 0, "maximum": 8, "description": "EXIF orientation, 1 to 8, or 0 for none." },
              "colourProfile": { "type": ["string", "null"], "description": "The ICC profile's description, or null for none." },
              "hasAlpha": { "type": "boolean" }
            }
          }
        }
      },
      "PurgeRequest": {
        "type": "object",
        "properties": {
          "target": {
            "type": "string",
            "description": "The object to purge: its path, its `/v1/` path or a delivery URL. Ending in `*` makes it a prefix."
          },
          "scope": {
            "type": "string",
            "enum": ["path", "prefix", "all"],
            "description": "Optional; inferred from `target`. `all` is reserved for the operator."
          }
        }
      },
      "PurgeResult": {
        "type": "object",
        "required": ["scope", "pattern", "entriesEvicted", "bytesFreed", "sharedEntriesEvicted", "pop"],
        "properties": {
          "scope": { "type": "string", "enum": ["path", "prefix", "all"] },
          "pattern": { "type": "string", "description": "The origin URL, or prefix, that was matched." },
          "entriesEvicted": { "type": "integer", "description": "Renditions removed from this node's memory." },
          "bytesFreed": { "type": "integer" },
          "sharedEntriesEvicted": { "type": "integer", "description": "Renditions removed from the store the fleet shares. Only the first node asked reports them." },
          "pop": { "type": "string", "description": "The node that answered." }
        }
      }
    }
  }
}
