{
  "openapi": "3.1.0",
  "info": {
    "title": "Pixabots API",
    "description": "Deterministic pixel character avatars. 10,752 unique combinations, each with a 4-char base36 ID.",
    "version": "0.1.0",
    "license": { "name": "MIT" },
    "contact": { "name": "Pablo Stanley", "url": "https://pablostanley.substack.com" }
  },
  "servers": [
    { "url": "https://pixabots.com", "description": "Production" }
  ],
  "paths": {
    "/api/pixabot/{id}": {
      "get": {
        "operationId": "getPixabot",
        "summary": "Get a pixabot by ID",
        "description": "Returns a pixel-perfect PNG image by default, or JSON metadata with `?format=json`. PNGs are CDN-cached (1-day fresh + 7-day stale-while-revalidate).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "4-char base36 pixabot ID (e.g. `2156`, `f76a`)",
            "schema": { "type": "string", "pattern": "^[0-9a-z]{4}$", "example": "2156" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Image size in pixels (any integer from 32 to 1920). Nearest-neighbor scaled from the 32×32 source. Multiples of 32 give the cleanest pixel-perfect result; other values still work.",
            "schema": { "type": "integer", "minimum": 32, "maximum": 1920, "default": 128 }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Response format. Omit for PNG (or GIF/WebP when animated). Use `json` for metadata, `svg` for vector output.",
            "schema": { "type": "string", "enum": ["json", "svg"] }
          },
          {
            "name": "animated",
            "in": "query",
            "required": false,
            "description": "Set to `true` to get an animated GIF of the bounce animation instead of a static PNG. Any other value (or absent) returns a static PNG. Any size from 32 to 1920, same as PNG.",
            "schema": { "type": "string", "enum": ["true", "false"] }
          },
          {
            "name": "speed",
            "in": "query",
            "required": false,
            "description": "Animation speed multiplier (0.25–4). Only applies when `animated=true`.",
            "schema": { "type": "number", "minimum": 0.25, "maximum": 4, "default": 1 }
          },
          {
            "name": "webp",
            "in": "query",
            "required": false,
            "description": "Set to `true` (together with `animated=true`) to get a lossless animated WebP instead of GIF. Smaller files, preserves alpha.",
            "schema": { "type": "string", "enum": ["true", "false"] }
          },
          {
            "name": "hue",
            "in": "query",
            "required": false,
            "description": "Hue rotation in degrees (0–359). Applies to PNG / GIF / WebP outputs.",
            "schema": { "type": "integer", "minimum": 0, "maximum": 359 }
          },
          {
            "name": "saturate",
            "in": "query",
            "required": false,
            "description": "Saturation multiplier (0 = greyscale, 1 = unchanged, up to 4).",
            "schema": { "type": "number", "minimum": 0, "maximum": 4, "default": 1 }
          },
          {
            "name": "bg",
            "in": "query",
            "required": false,
            "description": "Background color as `#rrggbb` hex. Flattens transparency onto a solid color. Applies to PNG / GIF / WebP / SVG outputs.",
            "schema": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }
          },
          {
            "name": "v",
            "in": "query",
            "required": false,
            "description": "Cache-bust key. Pass an integer matching `ANIM_VERSION` from `@pixabots/core`; the value is ignored by the renderer, so any change forces a new CDN cache entry and sidesteps the 1-day fresh / 8-day stale-while-revalidate window. `@pixabots/react` appends this automatically for animated output.",
            "schema": { "type": "integer", "minimum": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "Pixabot image (PNG or animated GIF) or metadata (JSON)",
            "content": {
              "image/png": {
                "schema": { "type": "string", "format": "binary" }
              },
              "image/gif": {
                "schema": { "type": "string", "format": "binary", "description": "Animated GIF (when animated=true)" }
              },
              "image/webp": {
                "schema": { "type": "string", "format": "binary", "description": "Animated WebP (when animated=true and webp=true)" }
              },
              "image/svg+xml": {
                "schema": { "type": "string", "description": "SVG vector output (when format=svg)" }
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "example": "2156" },
                    "combo": {
                      "type": "object",
                      "properties": {
                        "eyes": { "type": "integer" },
                        "heads": { "type": "integer" },
                        "body": { "type": "integer" },
                        "top": { "type": "integer" }
                      }
                    },
                    "parts": {
                      "type": "object",
                      "properties": {
                        "eyes": { "type": "string", "example": "glasses" },
                        "heads": { "type": "string", "example": "blob" },
                        "body": { "type": "string", "example": "wings" },
                        "top": { "type": "string", "example": "mohawk" }
                      }
                    },
                    "png": { "type": "string", "format": "uri", "description": "Static PNG URL at the requested size" },
                    "gif": { "type": "string", "format": "uri", "description": "Animated GIF URL at the requested size" }
                  },
                  "required": ["id", "combo", "parts", "png", "gif"]
                }
              }
            }
          },
          "400": {
            "description": "Invalid ID or size",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/og": {
      "get": {
        "operationId": "getOgImage",
        "summary": "Generate an Open Graph image",
        "description": "Returns a 1200×630 PNG suitable for social cards. Used by Pixabots' own pages but exposed publicly. Three layouts: a grid of seeded pixabots, a single hero pixabot, or a compare row of up to 6 specific pixabots with title/subtitle.",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Layout. `grid` (default) renders a 6×3 grid seeded by the `seed` param. `single` renders one large pixabot, requires `id`. `compare` renders up to 6 specific pixabots in a row, requires `ids`.",
            "schema": { "type": "string", "enum": ["grid", "single", "compare"], "default": "grid" }
          },
          {
            "name": "id",
            "in": "query",
            "required": false,
            "description": "Pixabot ID (required when `type=single`)",
            "schema": { "type": "string", "pattern": "^[0-9a-z]{4}$", "example": "2156" }
          },
          {
            "name": "ids",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of 1–6 pixabot IDs (required when `type=compare`). Invalid IDs are dropped.",
            "schema": { "type": "string", "example": "2156,f76a,0001" }
          },
          {
            "name": "title",
            "in": "query",
            "required": false,
            "description": "Headline text (max 60 chars, longer is truncated)",
            "schema": { "type": "string", "maxLength": 60, "default": "Pixabots" }
          },
          {
            "name": "subtitle",
            "in": "query",
            "required": false,
            "description": "Subtitle text (max 100 chars, longer is truncated)",
            "schema": { "type": "string", "maxLength": 100 }
          },
          {
            "name": "seed",
            "in": "query",
            "required": false,
            "description": "Seed for the grid layout's pixabot selection (max 80 chars). Defaults to `title` when omitted.",
            "schema": { "type": "string", "maxLength": 80 }
          }
        ],
        "responses": {
          "200": {
            "description": "1200×630 PNG image",
            "content": {
              "image/png": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "400": {
            "description": "Invalid type, missing/invalid id for single layout, or missing/invalid ids for compare layout",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "error": { "type": "string" } }
                }
              }
            }
          },
          "500": {
            "description": "OG render failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "error": { "type": "string" } }
                }
              }
            }
          }
        }
      }
    },
    "/api/pixabot/{id}/frames": {
      "get": {
        "operationId": "getPixabotFrames",
        "summary": "Get the animation timeline (JSON) for a pixabot",
        "description": "Returns the per-tick offsets, per-layer sprite-sheet frame indices, and sprite URLs needed to render the bounce animation client-side. Useful for consumers who want canvas / CSS-steps playback control without hitting the animated-GIF rate limit. Immutably cached.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "4-character base36 pixabot ID",
            "schema": { "type": "string", "pattern": "^[0-9a-z]{4}$" }
          }
        ],
        "responses": {
          "200": {
            "description": "Animation metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "example": "2156" },
                    "combo": {
                      "type": "object",
                      "properties": {
                        "eyes": { "type": "integer" },
                        "heads": { "type": "integer" },
                        "body": { "type": "integer" },
                        "top": { "type": "integer" }
                      }
                    },
                    "parts": {
                      "type": "object",
                      "properties": {
                        "eyes": { "type": "string" },
                        "heads": { "type": "string" },
                        "body": { "type": "string" },
                        "top": { "type": "string" }
                      }
                    },
                    "animVersion": { "type": "integer", "example": 1 },
                    "frameMs": { "type": "integer", "example": 72 },
                    "loopLength": { "type": "integer", "example": 16 },
                    "layerOrder": {
                      "type": "array",
                      "items": { "type": "string", "enum": ["top", "body", "heads", "eyes"] }
                    },
                    "nativeSize": { "type": "integer", "example": 32 },
                    "frames": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tick": { "type": "integer" },
                          "offsets": {
                            "type": "object",
                            "properties": {
                              "top": { "type": "number" },
                              "heads": { "type": "number" },
                              "eyes": { "type": "number" },
                              "body": { "type": "number" }
                            }
                          },
                          "spriteIndex": {
                            "type": "object",
                            "properties": {
                              "top": { "type": "integer" },
                              "heads": { "type": "integer" },
                              "eyes": { "type": "integer" },
                              "body": { "type": "integer" }
                            }
                          }
                        }
                      }
                    },
                    "sprites": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "url": { "type": "string", "format": "uri" },
                          "frames": { "type": "integer" },
                          "kind": { "type": "string", "enum": ["static", "blink", "sequence"] }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid pixabot ID",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "error": { "type": "string" } }
                }
              }
            }
          }
        }
      }
    },
    "/api/pixabot/batch": {
      "get": {
        "operationId": "getPixabotBatch",
        "summary": "Get metadata for many pixabots in one call",
        "description": "Returns JSON metadata (id, combo, parts, png, gif URLs) for a list of pixabots in a single request. Pass `ids=a,b,c,...` for specific ones (up to 100) or `count=N` for N random pixabots.",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of 4-char base36 pixabot IDs (max 100). Mutually exclusive with `count`.",
            "schema": { "type": "string", "example": "2156,f76a,0000" }
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Return N random pixabots (1–100). Mutually exclusive with `ids`.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100 }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Image size for the png/gif URLs embedded in the response (any integer from 32 to 1920).",
            "schema": { "type": "integer", "minimum": 32, "maximum": 1920, "default": 128 }
          }
        ],
        "responses": {
          "200": {
            "description": "Array of pixabot metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": { "type": "integer", "example": 3 },
                    "pixabots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string", "example": "2156" },
                          "combo": {
                            "type": "object",
                            "properties": {
                              "eyes": { "type": "integer" },
                              "heads": { "type": "integer" },
                              "body": { "type": "integer" },
                              "top": { "type": "integer" }
                            }
                          },
                          "parts": {
                            "type": "object",
                            "properties": {
                              "eyes": { "type": "string" },
                              "heads": { "type": "string" },
                              "body": { "type": "string" },
                              "top": { "type": "string" }
                            }
                          },
                          "png": { "type": "string", "format": "uri" },
                          "gif": { "type": "string", "format": "uri" }
                        },
                        "required": ["id", "combo", "parts", "png", "gif"]
                      }
                    }
                  },
                  "required": ["count", "pixabots"]
                }
              }
            }
          },
          "400": {
            "description": "Invalid size, missing both ids/count, too many IDs, or any invalid ID in the list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "error": { "type": "string" } }
                }
              }
            }
          }
        }
      }
    },
    "/api/pixabot/random": {
      "get": {
        "operationId": "getRandomPixabot",
        "summary": "Get a random pixabot",
        "description": "Returns a 302 redirect to a random pixabot's canonical URL (for caching). The `size`, `animated`, `speed`, `webp`, `hue`, `saturate`, and `bg` query params are forwarded to the redirect target. Use `?format=json` to get the random ID and metadata without redirect. The redirect itself is not cached (`Cache-Control: no-store`).",
        "parameters": [
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Image size in pixels, forwarded to the redirect target (any integer from 32 to 1920).",
            "schema": { "type": "integer", "minimum": 32, "maximum": 1920, "default": 128 }
          },
          {
            "name": "animated",
            "in": "query",
            "required": false,
            "description": "Forwarded to the redirect target. Set to `true` for an animated GIF.",
            "schema": { "type": "string", "enum": ["true", "false"] }
          },
          {
            "name": "speed",
            "in": "query",
            "required": false,
            "description": "Forwarded to the redirect target. Animation speed multiplier (0.25–4).",
            "schema": { "type": "number", "minimum": 0.25, "maximum": 4, "default": 1 }
          },
          {
            "name": "webp",
            "in": "query",
            "required": false,
            "description": "Forwarded to the redirect target. Set to `true` with `animated=true` for lossless animated WebP.",
            "schema": { "type": "string", "enum": ["true", "false"] }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Use `json` to get the random ID and metadata directly (no redirect).",
            "schema": { "type": "string", "enum": ["json"] }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the random pixabot's canonical URL (PNG by default, or GIF if `animated=true`). Sent with `Cache-Control: no-store` so each call returns a different pixabot.",
            "headers": {
              "Location": {
                "description": "Canonical URL of the chosen pixabot",
                "schema": { "type": "string", "format": "uri" }
              },
              "Cache-Control": {
                "schema": { "type": "string", "example": "no-store" }
              }
            }
          },
          "400": {
            "description": "Invalid forwarded size or speed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "error": { "type": "string" } }
                }
              }
            }
          },
          "200": {
            "description": "Random pixabot metadata (when format=json)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "example": "d632" },
                    "combo": {
                      "type": "object",
                      "properties": {
                        "eyes": { "type": "integer" },
                        "heads": { "type": "integer" },
                        "body": { "type": "integer" },
                        "top": { "type": "integer" }
                      }
                    },
                    "parts": {
                      "type": "object",
                      "properties": {
                        "eyes": { "type": "string" },
                        "heads": { "type": "string" },
                        "body": { "type": "string" },
                        "top": { "type": "string" }
                      }
                    },
                    "png": { "type": "string", "format": "uri", "description": "Static PNG URL for the random pixabot" },
                    "gif": { "type": "string", "format": "uri", "description": "Animated GIF URL for the random pixabot" }
                  },
                  "required": ["id", "combo", "parts", "png", "gif"]
                }
              }
            }
          }
        }
      }
    }
  }
}
