{"openapi":"3.1.0","info":{"title":"banja images","version":"0.1.0","description":"Contract-first image upload API for agentic QA workflows.\n\n### Install as a Claude Code skill\n\nDrop the contents below into `.claude/skills/upload-image/SKILL.md` in your repo, or grab it with curl:\n\n```bash\nmkdir -p .claude/skills/upload-image\ncurl -fsSL https://images.dev.banja.au/skill/upload-image.md \\\n  > .claude/skills/upload-image/SKILL.md\n```\n\n<details><summary><strong>SKILL.md</strong> — copy/paste</summary>\n\n````markdown\n---\nname: upload-image\ndescription: Use this skill when you have an image (typically a Playwright screenshot from a QA or debugging session) that needs to be hosted at a public URL so it can be embedded in a GitHub issue, PR comment, or Slack message. Triggers on phrases like \"include this screenshot in the issue\", \"attach this image\", or after capturing a screenshot during automated testing.\n---\n\n# Upload image to banja-images\n\nHosts an image at a stable public URL via the banja-images API. Returns a URL you can drop into Markdown (`![](url)`) for GitHub issues, PRs, or Slack.\n\n## Endpoint\n\n`POST https://images.dev.banja.au/v1/images`\n\n## Auth\n\nRequires a bearer token in the `BANJA_IMAGES_KEY` environment variable. Ask the maintainer for one if you don't have it — do not commit it.\n\n## Request\n\n- `Content-Type: multipart/form-data`\n- Form field name: `file` (required)\n- Allowed types: `image/png`, `image/jpeg`, `image/webp`, `image/gif` (sniffed from bytes — `Content-Type` from the client is ignored)\n- Max size: 10 MiB\n\n```bash\ncurl -X POST https://images.dev.banja.au/v1/images \\\n  -H \"Authorization: Bearer $BANJA_IMAGES_KEY\" \\\n  -F \"file=@./screenshot.png\"\n```\n\n## Response (201)\n\n```json\n{\n  \"id\": \"0192f8a0-7c3e-7000-8000-000000000000\",\n  \"url\": \"https://images.dev.banja.au/v1/images/0192f8a0-7c3e-7000-8000-000000000000\",\n  \"mime\": \"image/png\",\n  \"bytes\": 84213,\n  \"uploadedAt\": \"2026-04-15T04:32:11.000Z\"\n}\n```\n\nUse the `url` directly in Markdown:\n\n```markdown\n![Failing test on /checkout](https://images.dev.banja.au/v1/images/0192f8a0-7c3e-7000-8000-000000000000)\n```\n\nURLs are immutable and cached for a year — never reused.\n\n## Errors\n\nAll error responses are `application/problem+json` (RFC 7807):\n\n| Status | Meaning |\n|--------|---------|\n| 400 | Missing `file` field, or invalid request |\n| 401 | Missing/invalid bearer token |\n| 413 | File exceeds 10 MiB |\n| 415 | Not a recognised image, or SVG (not allowed — XSS risk) |\n\nExample:\n\n```json\n{\n  \"type\": \"https://images.dev.banja.au/errors/unsupported-media-type\",\n  \"title\": \"Unsupported Media Type\",\n  \"status\": 415,\n  \"detail\": \"Unsupported media type \\\"image/svg+xml\\\". Allowed: image/png, image/jpeg, image/webp, image/gif\"\n}\n```\n\n## Other operations\n\n- `GET /v1/images/{id}` — fetch bytes (public, no auth)\n- `GET /v1/images/{id}/metadata` — fetch JSON metadata (public)\n- `DELETE /v1/images/{id}` — delete (auth required)\n\n## Workflow for Playwright screenshots\n\nAfter capturing a screenshot via the Playwright MCP, upload it before referencing in an issue:\n\n1. Capture screenshot to a local path (e.g. `.playwright-mcp/foo.png`)\n2. POST it to `/v1/images` per above\n3. Use the returned `url` in the issue body — never reference the local path\n\nFull reference: https://images.dev.banja.au/docs\n````\n\n</details>\n\nSource: https://github.com/banja/images"},"servers":[{"url":"https://images.dev.banja.au","description":"dev"}],"tags":[{"name":"images","description":"Image upload, fetch, delete."},{"name":"health","description":"Liveness / readiness probes."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"BANJA_IMAGES_KEY — shared bearer token for write endpoints."}},"schemas":{"Health":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded"],"example":"ok"}},"required":["status"]},"ProblemDetails":{"type":"object","properties":{"type":{"type":"string","format":"uri","description":"A URI reference that identifies the problem type.","example":"https://images.dev.banja.au/errors/payload-too-large"},"title":{"type":"string","example":"Payload too large"},"status":{"type":"integer","example":413},"detail":{"type":"string","example":"Max upload size is 10485760 bytes"},"instance":{"type":"string","example":"/v1/images"}},"required":["type","title","status"]},"ImageUploadResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"UUIDv7 identifier for the image.","example":"0190f5d8-7b8a-7c2a-9f13-3a6a3e15b1a1"},"url":{"type":"string","format":"uri","description":"Public URL where the image is served.","example":"https://images.dev.banja.au/v1/images/0190f5d8-7b8a-7c2a-9f13-3a6a3e15b1a1"},"mime":{"type":"string","example":"image/png"},"bytes":{"type":"integer","minimum":0,"example":28754},"uploadedAt":{"type":"string","format":"date-time","example":"2026-04-14T02:33:12.006Z"}},"required":["id","url","mime","bytes","uploadedAt"]},"ImageUploadForm":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Image file. Sniffed server-side via magic bytes; allowed: image/png, image/jpeg, image/webp, image/gif."}}},"ImageMetadata":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"UUIDv7 identifier for the image.","example":"0190f5d8-7b8a-7c2a-9f13-3a6a3e15b1a1"},"mime":{"type":"string","description":"Sniffed MIME type.","example":"image/png"},"bytes":{"type":"integer","minimum":0,"description":"Size in bytes.","example":28754},"uploadedAt":{"type":"string","format":"date-time","description":"ISO 8601 upload timestamp.","example":"2026-04-14T02:33:12.006Z"},"originalFilename":{"type":"string","description":"Sanitized basename of the client-supplied filename.","example":"screenshot.png"}},"required":["id","mime","bytes","uploadedAt","originalFilename"]}},"parameters":{}},"paths":{"/healthz":{"get":{"summary":"Liveness probe","tags":["health"],"responses":{"200":{"description":"Process is up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}}}}},"/readyz":{"get":{"summary":"Readiness probe","tags":["health"],"responses":{"200":{"description":"MinIO bucket reachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"503":{"description":"MinIO bucket unreachable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/images":{"post":{"summary":"Upload an image","tags":["images"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/ImageUploadForm"}}}},"responses":{"201":{"description":"Image stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageUploadResponse"}}}},"400":{"description":"Invalid request.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"413":{"description":"Payload exceeds MAX_UPLOAD_BYTES.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"415":{"description":"Unsupported media type.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/images/{id}":{"get":{"summary":"Fetch image bytes","tags":["images"],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUIDv7 identifier for the image.","example":"0190f5d8-7b8a-7c2a-9f13-3a6a3e15b1a1"},"required":true,"description":"UUIDv7 identifier for the image.","name":"id","in":"path"}],"responses":{"200":{"description":"Image bytes.","content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"image/gif":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Image not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}},"delete":{"summary":"Delete an image","tags":["images"],"security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUIDv7 identifier for the image.","example":"0190f5d8-7b8a-7c2a-9f13-3a6a3e15b1a1"},"required":true,"description":"UUIDv7 identifier for the image.","name":"id","in":"path"}],"responses":{"204":{"description":"Deleted."},"401":{"description":"Missing or invalid bearer token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"Image not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/images/{id}/metadata":{"get":{"summary":"Fetch image metadata","tags":["images"],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUIDv7 identifier for the image.","example":"0190f5d8-7b8a-7c2a-9f13-3a6a3e15b1a1"},"required":true,"description":"UUIDv7 identifier for the image.","name":"id","in":"path"}],"responses":{"200":{"description":"Image metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageMetadata"}}}},"404":{"description":"Image not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}}},"webhooks":{}}