{
  "openapi": "3.1.0",
  "info": {
    "title": "OptimizeYourImage API",
    "version": "1.0.0",
    "description": "Synchronous image optimization without persistent image storage by default. Unless you explicitly set a storage mode, store=none returns the optimized bytes and stores neither source nor output. Opt-in store=temp returns JSON and retains only the optimized result with a download link valid for 2 hours. Access expiry is separate from physical deletion through storage lifecycle cleanup; no fixed deletion deadline is guaranteed. Opt-in store=cdn returns JSON and publishes the optimized result at a public CDN URL, retained until user deletion, Terms enforcement, or plan/subscription lifecycle cleanup. One successful image costs one credit; failed requests cost nothing.",
    "contact": { "email": "support@optimizeyourimage.com", "url": "https://optimizeyourimage.com/docs/api" },
    "termsOfService": "https://optimizeyourimage.com/terms"
  },
  "servers": [{ "url": "https://backend.optimizeyourimage.com" }],
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/v1/images/optimize": {
      "post": {
        "operationId": "optimizeImage",
        "summary": "Optimize one image",
        "description": "Accepts JPEG, PNG, WebP, HEIC, and HEIF up to 20 MB and 25 megapixels. HEIC/HEIF decoding uses the primary still image only. Send the image either as the multipart \"file\" part or as a public \"source_url\" the API fetches for you (exactly one of the two). Fetched source URLs are processed in memory and never logged or stored. With store=none (the default), the response body contains optimized image bytes and request metadata is returned in X-* headers. With store=temp, the response body is JSON containing a download URL for the optimized result. With store=cdn, the response body is JSON containing a public CDN URL for the optimized result.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "1-255 visible ASCII characters, unique per logical request (a UUID works). Retrying with the same key never double-bills: while the original is processing the retry gets 409 request-in-progress; after completion it gets 409 request-already-completed (image bytes are not retained, so the result is not replayed).",
            "schema": { "type": "string", "minLength": 1, "maxLength": 255 }
          },
          {
            "name": "X-Request-Id",
            "in": "header",
            "required": false,
            "description": "Optional client-supplied request id for tracing; echoed back if it matches a safe pattern, otherwise replaced with a generated id.",
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "Exactly one of \"file\" or \"source_url\" must be provided.",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The image. Format is detected from the bytes, not the filename. Max 20 MB encoded, 25 megapixels decoded. Mutually exclusive with source_url."
                  },
                  "source_url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "Public http(s) URL to fetch the image from instead of uploading it. Default ports only, no credentials, max 3 redirects, 20 MB cap, and private/internal addresses are refused (source-url-forbidden). The URL is not logged or stored. Mutually exclusive with file."
                  },
                  "output_format": {
                    "type": "string",
                    "enum": ["webp", "avif", "jpeg", "png"],
                    "default": "webp"
                  },
                  "quality": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 80
                  },
                  "max_width": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 25000,
                    "description": "Downscale to fit inside this width. Aspect ratio is preserved; images are never enlarged."
                  },
                  "max_height": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 25000,
                    "description": "Downscale to fit inside this height. Aspect ratio is preserved; images are never enlarged."
                  },
                  "preserve_metadata": {
                    "type": "boolean",
                    "default": false,
                    "description": "false strips EXIF/ICC/XMP (orientation is physically applied). true re-embeds the source EXIF into JPEG output with orientation normalized; ICC/XMP are not preserved, and non-JPEG outputs always strip metadata."
                  },
                  "store": {
                    "type": "string",
                    "enum": ["none", "temp", "cdn"],
                    "default": "none",
                    "description": "store=none returns raw bytes without persistent image storage (the default). store=temp stores only the optimized result and returns JSON with a download URL valid for 2 hours; access expiry is separate from physical deletion through storage lifecycle cleanup, with no fixed deletion deadline guaranteed. store=cdn returns JSON and publishes the result at cdn.optimizeyourimage.com, retained until user deletion, Terms enforcement, or plan/subscription lifecycle cleanup (counts against the plan's storage quota; list via GET /v1/images/cdn, delete via DELETE /v1/images/cdn/{file})."
                  },
                  "name": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$",
                    "description": "store=cdn only: customer-chosen base name for a stable URL (the output-format extension is appended, e.g. name=hero with webp publishes .../hero.webp). Without it a random name is generated. Re-publishing under the same name requires overwrite=true; note the URL changes if the output format changes."
                  },
                  "overwrite": {
                    "type": "boolean",
                    "default": false,
                    "description": "With store=cdn and name: replace the existing object under that name, keeping the same URL. Storage quota is charged as the size difference. Edge caches serve the old bytes for up to 24 hours after a replace. Without overwrite, a name collision returns 409 cdn-name-taken."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "With store=none (the default), returns optimized image bytes. With store=temp, returns JSON containing a download URL for the optimized result. With store=cdn, returns JSON containing a public CDN URL for the optimized result.",
            "headers": {
              "Content-Disposition": { "schema": { "type": "string" }, "description": "attachment; filename=\"<name>.<format>\"" },
              "X-Request-Id": { "schema": { "type": "string" } },
              "X-Input-Format": { "schema": { "type": "string", "enum": ["jpeg", "png", "webp", "heic", "heif"] } },
              "X-Output-Format": { "schema": { "type": "string", "enum": ["jpeg", "png", "webp", "avif"] } },
              "X-Original-Size": { "schema": { "type": "integer" }, "description": "Source image size in bytes (uploaded or fetched from source_url). On raw-bytes responses only; JSON store responses carry original_size in the body." },
              "X-Credits-Remaining": { "schema": { "type": "integer" } },
              "X-RateLimit-Limit": { "schema": { "type": "integer" } },
              "X-RateLimit-Remaining": { "schema": { "type": "integer" } },
              "X-RateLimit-Reset": { "schema": { "type": "string", "format": "date-time" } }
            },
            "content": {
              "image/webp": { "schema": { "type": "string", "format": "binary" } },
              "image/avif": { "schema": { "type": "string", "format": "binary" } },
              "image/jpeg": { "schema": { "type": "string", "format": "binary" } },
              "image/png": { "schema": { "type": "string", "format": "binary" } },
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Returned instead of raw bytes when store=temp or store=cdn.",
                  "required": ["id", "url", "store", "input_format", "format", "original_size", "optimized_size"],
                  "properties": {
                    "id": { "type": "string" },
                    "url": { "type": "string", "description": "Download URL (temp: expires; cdn: retained until user deletion, Terms enforcement, or plan/subscription lifecycle cleanup)." },
                    "store": { "type": "string", "enum": ["temp", "cdn"] },
                    "replaced": { "type": "boolean", "description": "store=cdn only: true when an existing object with the same name was replaced (overwrite=true)." },
                    "expires_in": { "type": "integer", "nullable": true, "description": "Seconds until the temp link expires; null for cdn." },
                    "input_format": { "type": "string" },
                    "format": { "type": "string", "description": "Format actually delivered." },
                    "requested_format": { "type": "string", "description": "Present only when the delivered format differs from the request (AVIF size ceiling fallback)." },
                    "original_size": { "type": "integer" },
                    "optimized_size": { "type": "integer" },
                    "width": { "type": "integer" },
                    "height": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Problem400" },
          "401": { "$ref": "#/components/responses/Problem401" },
          "402": { "$ref": "#/components/responses/Problem402" },
          "408": { "$ref": "#/components/responses/Problem408" },
          "409": { "$ref": "#/components/responses/Problem409" },
          "413": { "$ref": "#/components/responses/Problem413" },
          "415": { "$ref": "#/components/responses/Problem415" },
          "422": { "$ref": "#/components/responses/Problem422" },
          "429": { "$ref": "#/components/responses/Problem429" },
          "500": { "$ref": "#/components/responses/Problem500" },
          "503": { "$ref": "#/components/responses/Problem503" }
        }
      }
    },
    "/v1/images/downloads/{key}": {
      "get": {
        "operationId": "downloadTempResult",
        "summary": "Download a store=temp result",
        "description": "Unauthenticated download for store=temp results. Links expire 2 hours after creation.",
        "security": [],
        "parameters": [
          { "name": "key", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "The stored image bytes." },
          "404": { "description": "The download link is invalid or has expired (problem+json)." }
        }
      }
    },
    "/v1/images/cdn/domains": {
      "post": {
        "operationId": "addCdnDomain",
        "summary": "Add a custom CDN domain",
        "description": "Registers a customer-owned subdomain for CDN serving at https://<hostname>/i/<filename>. Apex/root domains are not supported. Create both returned CNAME records: the traffic record and Cloudflare's delegated TXT-validation record. Keep both for automatic certificate renewal, then check status until hostname and TLS are active. Domain allowance is per plan (Developer 1, Pro 3, Agency 10).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["hostname"],
                "properties": { "hostname": { "type": "string", "example": "images.example.com" } }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Domain registered; body includes traffic and delegated certificate-validation CNAME records plus the current status." },
          "400": { "$ref": "#/components/responses/Problem400" },
          "401": { "$ref": "#/components/responses/Problem401" },
          "402": { "description": "custom-domain-limit-exceeded (problem+json; limit and used extensions)." },
          "409": { "description": "custom-domain-taken (problem+json)." },
          "429": { "$ref": "#/components/responses/Problem429" },
          "503": { "$ref": "#/components/responses/Problem503" }
        }
      },
      "get": {
        "operationId": "listCdnDomains",
        "summary": "List custom CDN domains",
        "description": "Lists the account's domains, their independent hostname/TLS states, DNS record, and branded URL pattern. Ordinary calls only poll status; retry=1 also requests an immediate retry for pending validation.",
        "parameters": [
          { "name": "retry", "in": "query", "required": false, "description": "Set to 1 only for an explicit validation retry.", "schema": { "type": "string", "enum": ["1"] } }
        ],
        "responses": {
          "200": { "description": "{\"domains\": [{\"hostname\", \"status\", \"hostnameStatus\", \"sslStatus\", \"created_at\", \"urlPattern\", \"dns\": {\"type\", \"name\", \"target\", \"ttl\", \"validation\": {\"type\", \"name\", \"target\", \"ttl\"}}}]}" },
          "401": { "$ref": "#/components/responses/Problem401" },
          "409": { "description": "The Cloudflare hostname was deleted and must be removed and added again (problem+json)." },
          "429": { "$ref": "#/components/responses/Problem429" },
          "503": { "$ref": "#/components/responses/Problem503" }
        }
      }
    },
    "/v1/images/cdn/domains/{hostname}": {
      "delete": {
        "operationId": "deleteCdnDomain",
        "summary": "Remove a custom CDN domain",
        "parameters": [
          { "name": "hostname", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "204": { "description": "Removed. The hostname stops serving once edge config propagates." },
          "401": { "$ref": "#/components/responses/Problem401" },
          "404": { "description": "No custom domain with this hostname exists on this account (problem+json)." },
          "429": { "$ref": "#/components/responses/Problem429" },
          "503": { "$ref": "#/components/responses/Problem503" }
        }
      }
    },
    "/v1/images/cdn": {
      "get": {
        "operationId": "listCdnObjects",
        "summary": "List store=cdn objects",
        "description": "Lists the account's CDN objects newest-first with storage usage. Paginate with limit/offset.",
        "parameters": [
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 100 } },
          { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0 } }
        ],
        "responses": {
          "200": {
            "description": "The account's CDN objects and storage usage.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["objects", "total", "limit", "offset", "storage"],
                  "properties": {
                    "objects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "type": "string", "description": "File name — the {file} for DELETE /v1/images/cdn/{file}." },
                          "url": { "type": "string", "description": "Public CDN URL." },
                          "bytes": { "type": "integer" },
                          "content_type": { "type": "string" },
                          "created_at": { "type": "string", "format": "date-time", "description": "Upload time; updated when the object is replaced via overwrite." }
                        }
                      }
                    },
                    "total": { "type": "integer", "description": "Total objects on the account, ignoring pagination." },
                    "limit": { "type": "integer" },
                    "offset": { "type": "integer" },
                    "storage": {
                      "type": "object",
                      "properties": {
                        "used": { "type": "integer", "description": "Bytes used." },
                        "limit": { "type": "integer", "description": "Plan quota in bytes." }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Problem400" },
          "401": { "$ref": "#/components/responses/Problem401" },
          "503": { "$ref": "#/components/responses/Problem503" }
        }
      }
    },
    "/v1/images/cdn/{file}": {
      "delete": {
        "operationId": "deleteCdnObject",
        "summary": "Delete a store=cdn object",
        "description": "Removes the object from the CDN and releases its storage quota. Edge caches age out within 24 hours.",
        "parameters": [
          { "name": "file", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The file name from the store=cdn URL (last path segment)." }
        ],
        "responses": {
          "204": { "description": "Deleted." },
          "401": { "$ref": "#/components/responses/Problem401" },
          "404": { "description": "No CDN object with this name exists on this account (problem+json)." },
          "503": { "$ref": "#/components/responses/Problem503" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from the dashboard, prefixed oyi_live_. Shown once at creation; only a hash is stored."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 7807 problem details. Every type URI resolves to a documentation page under https://optimizeyourimage.com/docs/problems/. Extra members appear per type: rate-limit-exceeded adds limit/remaining/resetAt (also promoted to X-RateLimit-* and Retry-After headers); insufficient-credits adds creditsRemaining (also X-Credits-Remaining); idempotency conflicts add originalRequestId.",
        "properties": {
          "type": { "type": "string", "format": "uri" },
          "title": { "type": "string" },
          "status": { "type": "integer" },
          "detail": { "type": "string" },
          "instance": { "type": "string" },
          "requestId": { "type": "string" }
        },
        "required": ["type", "title", "status", "detail"],
        "additionalProperties": true
      }
    },
    "responses": {
      "Problem400": {
        "description": "file-required, invalid-multipart-request, invalid-idempotency-key, source-url-invalid, source-url-forbidden, or generic http-400 validation errors.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Problem401": {
        "description": "Missing, malformed, or revoked API key (generic http-401).",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Problem402": {
        "description": "insufficient-credits (X-Credits-Remaining header is set) or storage-quota-exceeded (store=cdn; limit and used extensions carry bytes).",
        "headers": { "X-Credits-Remaining": { "schema": { "type": "integer" } } },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Problem408": {
        "description": "processing-timeout: the 30-second deadline was exceeded. No credit charged.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Problem409": {
        "description": "request-in-progress or request-already-completed (Idempotency-Key reuse), or cdn-name-taken (store=cdn name collision without overwrite=true).",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Problem413": {
        "description": "image-too-large: over 20 MB encoded (uploaded or fetched from source_url) or 25 megapixels decoded.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Problem415": {
        "description": "unsupported-input-format, unsupported-output-format, or heic-decoder-unavailable.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Problem422": {
        "description": "invalid-image (undecodable file), idempotency-key-conflict, or source-url-fetch-failed (DNS failure, upstream error status, empty body, or timeout).",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Problem429": {
        "description": "rate-limit-exceeded (with X-RateLimit-* and Retry-After headers) or concurrency-limit-exceeded (Retry-After only).",
        "headers": {
          "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait before retrying." },
          "X-RateLimit-Limit": { "schema": { "type": "integer" } },
          "X-RateLimit-Remaining": { "schema": { "type": "integer" } },
          "X-RateLimit-Reset": { "schema": { "type": "string", "format": "date-time" } }
        },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Problem500": {
        "description": "internal-server-error. No credit charged; safe to retry with a new Idempotency-Key.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Problem503": {
        "description": "Credit accounting or a dependency is temporarily unavailable (generic http-503). Safe to retry.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      }
    }
  }
}
