{
  "openapi": "3.1.0",
  "info": {
    "title": "Zebra Public API",
    "version": "1.0.0",
    "summary": "Server-side photo and PDF processing endpoints for Zebra.",
    "description": "Public HTTP API for Zebra (https://zebra.tg), a free browser-based AI photo editor.\n\n**No authentication.** There are no API keys, tokens, or OAuth flows. Every endpoint documented here is callable anonymously — see https://zebra.tg/auth.md.\n\n**Scope.** Only Zebra's *server-side* tools appear here. Most Zebra tools (filters, crop, blur, darken, grain, bubbles, tags, collage, effects, draw-with-text, image compression, PDF merge) run entirely client-side in the browser via Canvas/WASM and therefore have no HTTP endpoint — for those, open the tool page directly. Canonical per-tool URLs are listed in https://zebra.tg/.well-known/agent-skills/index.json.\n\n**Rate limits.** Limits are enforced per IP and per anonymous cookie, not per identity. Exceeding one returns `429` with a `Retry-After` header; back off and retry.\n\n**Calling from a script or agent.** State-changing requests (POST) require an `Origin: https://zebra.tg` header as a CSRF speed-bump. Agents and CLI clients should send it explicitly:\n\n```sh\ncurl -X POST https://zebra.tg/api/compress-pdf?tier=medium \\\n  -H 'Origin: https://zebra.tg' \\\n  -F 'file=@input.pdf' -o output.pdf\n```\n\nOmitting the header returns `403 {\"error\":\"forbidden origin\"}`. GET endpoints need no header.\n\n**MCP.** Zebra also speaks Model Context Protocol at `POST /api/mcp` over JSON-RPC 2.0 (`initialize`, `tools/list`, `tools/call`), unauthenticated. See https://zebra.tg/api-docs.md#mcp.",
    "termsOfService": "https://zebra.tg/terms/",
    "contact": {
      "name": "Zebra support",
      "url": "https://zebra.tg/contact/",
      "email": "probiserov@gmail.com"
    },
    "license": {
      "name": "Proprietary — free to call, see Terms",
      "url": "https://zebra.tg/terms/"
    }
  },
  "servers": [
    {
      "url": "https://zebra.tg",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Agent-facing site documentation (llms.txt)",
    "url": "https://zebra.tg/llms-full.txt"
  },
  "security": [],
  "tags": [
    {
      "name": "status",
      "description": "Service health and quota introspection. No side effects."
    },
    {
      "name": "image",
      "description": "Server-side image processing."
    },
    {
      "name": "pdf",
      "description": "Server-side PDF processing."
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Service health check",
        "description": "Liveness probe. Returns the service status and version. No authentication, no rate limit, no side effects.",
        "tags": [
          "status"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "example": {
                  "status": "ok",
                  "version": "0.1.0"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/features": {
      "get": {
        "operationId": "listFeatures",
        "summary": "List editor features",
        "description": "Returns the catalogue of editor features surfaced on the Zebra homepage. Useful for an agent that wants to describe what Zebra can do without scraping HTML.",
        "tags": [
          "status"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Feature catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeatureList"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/usage/limits": {
      "get": {
        "operationId": "getUsageLimits",
        "summary": "Current quota and remaining usage",
        "description": "Reports the caller's remaining daily quota for metered tools. Identity is derived from the anonymous `zebra_anon` cookie (or a signed-in session, if one is present); a fresh caller with no cookies gets the anonymous free-tier allowance.",
        "tags": [
          "status"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Quota state for the calling identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageLimits"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/compress-pdf": {
      "post": {
        "operationId": "compressPdf",
        "summary": "Compress a PDF",
        "description": "Reduces PDF file size using a Ghostscript distiller preset. Returns the compressed PDF as a binary body.\n\nIf a tier does not actually shrink the input, the original bytes are returned unchanged — the response is never larger than the input. Compare the `X-Original-Size` and `X-Compressed-Size` response headers to see what was achieved.\n\nMaximum input size is 150 MB.",
        "tags": [
          "pdf"
        ],
        "security": [],
        "parameters": [
          {
            "name": "tier",
            "in": "query",
            "required": false,
            "description": "Compression strength. `light` targets ~150 dpi and preserves print quality; `medium` and `strong` target ~72 dpi for screen viewing.",
            "schema": {
              "type": "string",
              "enum": [
                "light",
                "medium",
                "strong"
              ],
              "default": "medium"
            }
          },
          {
            "$ref": "#/components/parameters/OriginHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The PDF to compress. Must begin with the `%PDF-` magic bytes and be at most 150 MB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The compressed PDF.",
            "headers": {
              "X-Original-Size": {
                "description": "Size of the uploaded PDF in bytes.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Compressed-Size": {
                "description": "Size of the returned PDF in bytes. Equal to X-Original-Size when compression would not have helped.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Missing/empty file, a non-PDF payload (`not_pdf`), or an unrecognised tier (`unknown_tier`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "not_pdf"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenOrigin"
          },
          "413": {
            "description": "Input exceeds the 150 MB limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "too_large"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/remove-bg": {
      "post": {
        "operationId": "removeBackground",
        "summary": "Remove an image background",
        "description": "Removes the background from a photo using an AI segmentation model and returns a transparent PNG at the source resolution.\n\nThe `standard` quality tier is available to anonymous callers. The `hd` tier is reserved for signed-in premium accounts and is not reachable anonymously — an anonymous request for `hd` is answered with a quota/upgrade response rather than an image.\n\nMaximum input size is 20 MB.",
        "tags": [
          "image"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/OriginHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Source image (JPEG, PNG, or WebP), at most 20 MB."
                  },
                  "quality": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "hd"
                    ],
                    "default": "standard",
                    "description": "`standard` is free and anonymous. `hd` requires an authenticated premium session."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cutout image with a transparent background.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "The upload is missing or is not a decodable image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The requested tier requires premium, or the free allowance is exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenOrigin"
          },
          "413": {
            "description": "Input exceeds the 20 MB limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/auto-enhance": {
      "post": {
        "operationId": "autoEnhanceImage",
        "summary": "Auto-enhance a photo (streaming)",
        "description": "Runs several enhancement models over one photo and streams each result back as it finishes, using Server-Sent Events (`text/event-stream`).\n\nEach `result` event carries one model's output as a base64 JPEG data URI, so a client can show the first result without waiting for the slowest model. A final `done` event closes the stream. A model that fails yields a `result` event carrying an `error` field instead of an `image` — a partial failure does not abort the stream.\n\nMaximum input size is 20 MB.",
        "tags": [
          "image"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/OriginHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Source image (JPEG, PNG, or WebP), at most 20 MB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An SSE stream of per-model enhancement results.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "example": "event: result\ndata: {\"model\": \"clarity\", \"image\": \"data:image/jpeg;base64,...\"}\n\nevent: done\ndata: {}\n\n"
              }
            }
          },
          "400": {
            "description": "The upload is missing or is not a decodable image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenOrigin"
          },
          "413": {
            "description": "Input exceeds the 20 MB limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "OriginHeader": {
        "name": "Origin",
        "in": "header",
        "required": true,
        "description": "Must be `https://zebra.tg`. A CSRF speed-bump applied to all state-changing requests; scripted and agent clients should send it explicitly.",
        "schema": {
          "type": "string",
          "default": "https://zebra.tg"
        }
      }
    },
    "responses": {
      "ForbiddenOrigin": {
        "description": "The request carried no `Origin`/`Referer` header, or one that is not `https://zebra.tg`. Resend with `Origin: https://zebra.tg`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "forbidden origin"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded for this IP or anonymous identity. Honour `Retry-After` and back off.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "rate_limit",
              "message": "Too many requests, try again later"
            }
          }
        }
      }
    },
    "schemas": {
      "Health": {
        "type": "object",
        "title": "Health",
        "required": [
          "status",
          "version"
        ],
        "properties": {
          "status": {
            "type": "string",
            "description": "`ok` when the service is healthy.",
            "examples": [
              "ok"
            ]
          },
          "version": {
            "type": "string",
            "description": "Deployed API version.",
            "examples": [
              "0.1.0"
            ]
          }
        }
      },
      "Feature": {
        "type": "object",
        "title": "Feature",
        "required": [
          "id",
          "title",
          "description",
          "available"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable feature identifier.",
            "examples": [
              "remove-bg"
            ]
          },
          "title": {
            "type": "string",
            "examples": [
              "Remove Background"
            ]
          },
          "description": {
            "type": "string"
          },
          "icon": {
            "type": "string",
            "description": "Icon name used by the web UI."
          },
          "available": {
            "type": "boolean",
            "description": "Whether the feature is live."
          }
        }
      },
      "FeatureList": {
        "type": "object",
        "title": "FeatureList",
        "required": [
          "features"
        ],
        "properties": {
          "features": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Feature"
            }
          }
        }
      },
      "UsageLimits": {
        "type": "object",
        "title": "UsageLimits",
        "description": "Remaining daily allowance for metered tools, for the calling identity.",
        "additionalProperties": true,
        "properties": {
          "remaining": {
            "type": "integer",
            "description": "Operations left in the current UTC day."
          },
          "limit": {
            "type": "integer",
            "description": "Total allowance for the current UTC day."
          },
          "tier": {
            "type": "string",
            "description": "Identity tier, e.g. `anon`, `free`, or `premium`."
          }
        }
      },
      "Error": {
        "type": "object",
        "title": "Error",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable error code.",
            "examples": [
              "rate_limit"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation, when available."
          }
        }
      }
    }
  }
}
