{"openapi":"3.1.0","info":{"title":"Tilery API","description":"Vector map tile serving with JWT auth and usage-based rate limiting.","license":{"name":""},"version":"0.1.0"},"servers":[{"url":"https://api.tilery.eu","description":"Current environment"}],"paths":{"/health":{"get":{"tags":["health"],"summary":"Readiness probe: reports whether this instance can actually serve tiles.","description":"Three things consume it, and all three take a box out of service on a\nnon-200: Caddy's active health check drops the container from its upstream\npool, the weekly archive sync refuses to flip the A/B symlink, and\nScaleway's health-checked DNS record removes the box from the\n`api.tilery.eu` rotation. That last one is why the body wording is a\ncontract and not cosmetic — the record matches on the literal substring\n`\"status\":\"healthy\"`.\n\nSo this reads the archive rather than reporting that the process is up. A\nbox whose archive died after boot answers every tile request with an error\nwhile remaining perfectly alive, and that is exactly the state worth\ndraining traffic away from.","operationId":"health_handler","responses":{"200":{"description":"Serving: the PMTiles archive is readable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"503":{"description":"Not serving: the PMTiles archive could not be read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/map/fonts/{fontstack}/{range}":{"get":{"tags":["map-assets"],"summary":"Serves font glyph files from the assets directory\nPath format: /map/fonts/{fontstack}/{range}.pbf","operationId":"serve_font","parameters":[{"name":"fontstack","in":"path","description":"Comma-separated font stack (e.g. 'Noto Sans Regular')","required":true,"schema":{"type":"string"}},{"name":"range","in":"path","description":"Glyph codepoint range (e.g. '0-255.pbf')","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Font glyph protobuf","content":{"application/x-protobuf":{}}},"400":{"description":"Invalid path"},"404":{"description":"Font not found"}}}},"/map/sprites/{version}/{theme}":{"get":{"tags":["map-assets"],"summary":"Serves sprite files from the assets directory\nPath format: /map/sprites/{version}/{theme}.{extension}\nExamples: /map/sprites/v4/light.json, /map/sprites/v4/light@2x.png","operationId":"serve_sprite","parameters":[{"name":"version","in":"path","description":"Sprite sheet version (e.g. 'v4')","required":true,"schema":{"type":"string"}},{"name":"theme","in":"path","description":"Theme name with extension (e.g. 'light.json', 'light@2x.png')","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sprite sheet JSON or PNG"},"400":{"description":"Invalid path"},"404":{"description":"Sprite not found"}}}},"/map/styles":{"get":{"tags":["map-assets"],"summary":"List all available styles.","description":"Returns a JSON array of style entries with id, name, url, and metadata.\n`GET /styles`","operationId":"list_styles","responses":{"200":{"description":"List of available styles","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StyleEntry"}}}}}}}},"/map/styles/{name}":{"get":{"tags":["map-assets"],"summary":"Serve a single style JSON with base URL placeholders replaced.","description":"`GET /styles/{name}`","operationId":"serve_style","parameters":[{"name":"name","in":"path","description":"Style identifier (lowercase alphanumeric + hyphens)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"MapLibre-compatible style JSON","content":{"application/json":{}}},"400":{"description":"Invalid style name"},"404":{"description":"Style not found"}}}},"/map/tiles/vector.json":{"get":{"tags":["map-assets"],"summary":"Serve the vector tileset's TileJSON descriptor.","description":"`GET /map/tiles/vector.json`","operationId":"serve_tilejson","responses":{"200":{"description":"TileJSON 3.0 descriptor for the vector tileset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TileJson"}}}}}}},"/map/tiles/vector/{z}/{x}/{y}":{"get":{"tags":["tiles"],"operationId":"tile_handler","parameters":[{"name":"z","in":"path","description":"Zoom level (0–22).","required":true,"schema":{"type":"integer","format":"int32","minimum":0}},{"name":"x","in":"path","description":"Tile x coordinate.","required":true,"schema":{"type":"integer","format":"int32","minimum":0}},{"name":"y","in":"path","description":"Tile y coordinate.","required":true,"schema":{"type":"integer","format":"int32","minimum":0}},{"name":"token","in":"query","description":"Short-lived JWT issued by the web app's /api/tokens/exchange endpoint, or a raw app-mode API key.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Vector tile (MVT/protobuf)","content":{"application/x-protobuf":{}}},"400":{"description":"Invalid tile coordinates"},"401":{"description":"Missing or invalid token (JWT or API key)"},"402":{"description":"Credit balance exhausted (past the grace allowance)"},"403":{"description":"Origin not allowed for this token"},"404":{"description":"Tile not present in archive"},"429":{"description":"Rate limit exceeded (per-IP request rate, or blocked after repeated auth failures)"}},"security":[{"bearer_token":[]}]}}},"components":{"schemas":{"HealthResponse":{"type":"object","required":["status","version"],"properties":{"status":{"type":"string"},"version":{"type":"string"}}},"StyleEntry":{"type":"object","required":["id","name","url"],"properties":{"id":{"type":"string","description":"Stable identifier (slug) — matches the filename stem."},"metadata":{"description":"Optional style metadata from the style JSON."},"name":{"type":"string","description":"Human-readable name of the style."},"url":{"type":"string","description":"Absolute URL to the style JSON endpoint."}}},"TileJson":{"type":"object","description":"TileJSON 3.0 descriptor.\n\nA **token-less** skeleton: the `tiles` template points at the public tile\nroute with NO auth embedded. Consumers supply the short-lived access token\nthemselves (the `@tilery/client` SDK injects `?token=`); the Rust server is\ndeliberately auth-free for descriptors and cannot mint the ≤15-minute token.\n\n`minzoom`/`maxzoom`/`bounds`/`attribution` are static here, matching the\ngenerated styles. A future enhancement can derive them (and `vector_layers`)\nfrom the PMTiles header + metadata via `serde_json` — no new cargo dep.","required":["tilejson","name","scheme","tiles","minzoom","maxzoom","bounds","attribution"],"properties":{"attribution":{"type":"string"},"bounds":{"type":"array","items":{"type":"number","format":"double"},"description":"`[west, south, east, north]` in WGS84 degrees."},"maxzoom":{"type":"integer","format":"int32","minimum":0},"minzoom":{"type":"integer","format":"int32","minimum":0},"name":{"type":"string"},"scheme":{"type":"string","description":"Tiling scheme — always \"xyz\" (web-mercator, top-left origin)."},"tilejson":{"type":"string","description":"TileJSON spec version."},"tiles":{"type":"array","items":{"type":"string"},"description":"Tile URL template(s) with `{z}/{x}/{y}` placeholders; token-less."}}}},"securitySchemes":{"bearer_token":{"type":"apiKey","in":"query","name":"token","description":"Short-lived JWT issued by the web app's /api/tokens/exchange endpoint."}}},"tags":[{"name":"tiles","description":"Authenticated vector tile delivery"},{"name":"map-assets","description":"Public fonts, sprites, and style JSONs"},{"name":"health","description":"Readiness probe — reads the tile archive"}]}