{
    "openapi": "3.1.0",
    "info": {
        "title": "Shareglint",
        "version": "1.0.0",
        "description": "Open Graph images from a single HTTP request.\n\n**How long images live.** Free plan: 30 days from generation. Paid plans: 365 days. After that both the file and the history entry are gone, and the same render ordered again no longer hits the cache and counts against your quota."
    },
    "servers": [
        {
            "url": "https://shareglint.com/v1"
        }
    ],
    "security": [
        {
            "bearerAuth": []
        }
    ],
    "components": {
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "description": "API key in the `Authorization: Bearer sk_live_...` header."
            }
        },
        "schemas": {
            "Render": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "example": "img_01m08s3ez1zcv28h335g9923bw"
                    },
                    "kind": {
                        "type": "string",
                        "enum": [
                            "image",
                            "screenshot"
                        ],
                        "description": "`image` = a template render, `screenshot` = a web page screenshot (`POST /screenshots`)."
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "queued",
                            "processing",
                            "completed",
                            "failed"
                        ]
                    },
                    "format": {
                        "type": "string",
                        "enum": [
                            "png",
                            "jpeg",
                            "webp",
                            "pdf"
                        ],
                        "description": "File format of the render, as requested. Renders made before formats existed are `png`."
                    },
                    "url": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "URL of the finished file (`.png`, `.jpg`, `.webp` or `.pdf`, served with the matching Content-Type). Present once the status is completed."
                    },
                    "error": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Always English — this is a database row, not an HTTP response."
                    },
                    "duration_ms": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "created_at": {
                        "type": "string"
                    },
                    "completed_at": {
                        "type": [
                            "string",
                            "null"
                        ]
                    }
                }
            },
            "Template": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "example": "tpl_01m08r57bvcppea9w35qrrctcv"
                    },
                    "slug": {
                        "type": "string",
                        "example": "event-card"
                    },
                    "name": {
                        "type": "string"
                    },
                    "width": {
                        "type": "integer"
                    },
                    "height": {
                        "type": "integer"
                    },
                    "engine": {
                        "type": "string",
                        "enum": [
                            "blade",
                            "html"
                        ],
                        "description": "blade = built-in template, html = yours."
                    },
                    "editable": {
                        "type": "boolean"
                    },
                    "fields": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "images": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Fields that take an image URL."
                    },
                    "defaults": {
                        "type": [
                            "object",
                            "array"
                        ],
                        "description": "Field name => value used when you leave that field out of `modifications`. An empty array (`[]`) when there are none."
                    },
                    "view": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Built-in templates only: the layout they render with, e.g. `templates.blog-og`. `null` for yours."
                    },
                    "html": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The sanitised version — the one that actually renders. `null` for built-in templates."
                    },
                    "css": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "editor": {
                        "type": "string",
                        "enum": [
                            "visual",
                            "code"
                        ],
                        "description": "`visual` = made in the dashboard's visual editor, `code` = HTML written by hand (and every built-in template)."
                    },
                    "editor_model": {
                        "type": [
                            "object",
                            "null"
                        ],
                        "description": "Only in `GET /templates/{id}`: the visual editor's model the HTML was compiled from, `null` for code templates."
                    },
                    "created_at": {
                        "type": "string"
                    },
                    "updated_at": {
                        "type": "string"
                    }
                }
            },
            "Error": {
                "type": "object",
                "properties": {
                    "error": {
                        "type": "string",
                        "description": "A stable machine-readable code. Never translated, never changed."
                    },
                    "message": {
                        "type": "string",
                        "description": "Human-readable text. Its language follows the Accept-Language header."
                    }
                }
            }
        }
    },
    "tags": [
        {
            "name": "Images",
            "description": "Requesting renders and collecting results."
        },
        {
            "name": "Templates",
            "description": "Your own HTML templates with `{{ }}` fields."
        },
        {
            "name": "Account",
            "description": "Quota and API keys."
        },
        {
            "name": "Analytics",
            "description": "How often your images were fetched, and by which platforms."
        },
        {
            "name": "Auto OG",
            "description": "One `og:image` tag for a whole site: allowed domains and the `/u/` image URL."
        },
        {
            "name": "Screenshots",
            "description": "Screenshots of public web pages: PNG, JPEG, WebP or PDF, viewport or full page."
        }
    ],
    "paths": {
        "/images": {
            "post": {
                "tags": [
                    "Images"
                ],
                "operationId": "createImage",
                "summary": "Request a render",
                "description": "Asynchronous by default: you get a 202 and an `id`, then collect the result from `GET /images/{id}`. With `?wait=true` we hold the connection until the image is ready and return 200 — or 503 `sync_overloaded` if we run out of time. `format`, `quality` and `scale` are part of the cache key: the same fields as PNG and as JPEG are two files and two renders, while a parameter the format ignores (`quality` for PNG) never makes a second one.",
                "parameters": [
                    {
                        "name": "wait",
                        "in": "query",
                        "schema": {
                            "type": "boolean"
                        },
                        "description": "Wait for the result instead of polling."
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "schema": {
                            "type": "string"
                        },
                        "description": "The same `Idempotency-Key` with the same body returns the same response, with no second render. An empty or whitespace-only header counts as no key."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "template"
                                ],
                                "properties": {
                                    "template": {
                                        "type": "string",
                                        "description": "Template slug: one of yours or a built-in one from `GET /templates/library`.",
                                        "example": "blog-og"
                                    },
                                    "modifications": {
                                        "type": "object",
                                        "description": "Values for the template fields. A field you leave out takes the template's `defaults` value, or stays empty — omitting `modifications` altogether renders the defaults."
                                    },
                                    "locale": {
                                        "type": "string",
                                        "enum": [
                                            "en",
                                            "pl",
                                            "de",
                                            "es",
                                            "fr"
                                        ],
                                        "description": "Language for text the template adds itself (date, reading time)."
                                    },
                                    "webhook_url": {
                                        "type": "string",
                                        "description": "We call this URL when the render finishes. Private-network addresses are rejected with a 422 at request time, so a webhook that could never fire never costs you a render."
                                    },
                                    "format": {
                                        "type": "string",
                                        "enum": [
                                            "png",
                                            "jpeg",
                                            "webp",
                                            "pdf"
                                        ],
                                        "default": "png",
                                        "description": "Output file. `pdf` is a single page exactly the template size in CSS px (1200×630 for the built-in templates), no margins, backgrounds printed, text kept as vector. The Free plan watermark applies to every format."
                                    },
                                    "quality": {
                                        "type": "integer",
                                        "minimum": 1,
                                        "maximum": 100,
                                        "default": 85,
                                        "description": "Compression quality for `jpeg` and `webp`. Ignored for `png` (lossless) and `pdf`."
                                    },
                                    "scale": {
                                        "type": "integer",
                                        "enum": [
                                            1,
                                            2
                                        ],
                                        "default": 2,
                                        "description": "Pixel density for `png`, `jpeg` and `webp`: 2 gives 2400×1260 for a 1200×630 template, 1 gives exactly 1200×630. Ignored for `pdf`."
                                    }
                                }
                            },
                            "example": {
                                "template": "blog-og",
                                "locale": "en",
                                "modifications": {
                                    "eyebrow": "Health",
                                    "title": "Ten things that ruin your sleep",
                                    "author": "Anna Kowalska",
                                    "site": "yourblog.com",
                                    "accent": "#f97316",
                                    "date": "2026-08-17",
                                    "reading_time": 7
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Finished (only with `?wait=true`).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Render"
                                }
                            }
                        }
                    },
                    "202": {
                        "description": "Accepted into the queue.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "id": "img_01m08…",
                                    "status": "queued",
                                    "format": "png",
                                    "url": "…/v1/images/img_01m08…"
                                }
                            }
                        }
                    },
                    "402": {
                        "description": "Monthly quota exhausted (`quota_exceeded`).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation error (including an unknown `format`, `quality` outside 1–100 or `scale` other than 1 or 2), or the same `Idempotency-Key` with a different body."
                    },
                    "503": {
                        "description": "Sync mode ran out of time (`sync_overloaded`). The body carries `id` — nothing is lost."
                    }
                }
            },
            "get": {
                "tags": [
                    "Images"
                ],
                "operationId": "listImages",
                "summary": "Render history",
                "description": "Cursor pagination: pass `before` from the `next_cursor` field. Renders arriving while you page through will not duplicate results.",
                "parameters": [
                    {
                        "name": "limit",
                        "in": "query",
                        "schema": {
                            "type": "integer",
                            "default": 25,
                            "maximum": 100
                        }
                    },
                    {
                        "name": "before",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Cursor from `next_cursor`."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of renders, newest first.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/Render"
                                            }
                                        },
                                        "next_cursor": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "Pass it as `before` for the next page. `null` on the last page."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/images/{id}": {
            "get": {
                "tags": [
                    "Images"
                ],
                "operationId": "getImage",
                "summary": "Render status",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The render.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Render"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No such render on this account."
                    }
                }
            }
        },
        "/templates": {
            "get": {
                "tags": [
                    "Templates"
                ],
                "operationId": "listTemplates",
                "summary": "List templates",
                "responses": {
                    "200": {
                        "description": "Templates on this account, both built-in and your own, oldest first. Not paginated. Built-in templates from the library (`GET /templates/library`) work by slug without being listed here; the first render of one adds it to this list as a read-only `engine: blade` template.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/Template"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "tags": [
                    "Templates"
                ],
                "operationId": "createTemplate",
                "summary": "Create a template",
                "description": "You send your own HTML and CSS. Fields go in as `{{ name }}` and **must be declared** in `fields` — a typo fails with a 422 on save rather than leaving a blank spot in the image.\n\nThe HTML is sanitised on save (`<script>`, `<iframe>` and `on*` attributes are dropped), and the CSS loses `@import`, `@font-face` and any `url()` outside `data:`. We store the sanitised version — the `html` field in the response returns it.\n\nLimits: HTML and CSS up to 64 KB each, dimensions from 200 to 4000 px.\n\nInstead of `html`/`css`/`fields` you can send `editor_model` — the layer model the dashboard's visual editor produces (copy it from `GET /templates/{id}`). We validate it, compile it to HTML and CSS, declare the fields ourselves and sanitise the result like any other template.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "example": {
                                "name": "Event card",
                                "width": 1200,
                                "height": 630,
                                "fields": [
                                    "headline",
                                    "place",
                                    "photo"
                                ],
                                "images": [
                                    "photo"
                                ],
                                "css": "body{background:#111827;color:#f9fafb;padding:72px;font-family:Roboto} h1{font-size:88px;font-weight:900}",
                                "html": "<div class=\"fitbox\"><h1 class=\"fit\" data-autofit>{{ headline }}</h1></div><img src=\"{{ photo }}\"><span>{{ place }}</span>"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Created.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Template"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "A field used in the HTML is not declared, `images` goes beyond `fields`, or a size limit was exceeded."
                    }
                }
            }
        },
        "/templates/{id}": {
            "get": {
                "tags": [
                    "Templates"
                ],
                "operationId": "getTemplate",
                "summary": "Template details",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The template, with `editor_model`.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Template"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No such template on this account."
                    }
                }
            },
            "patch": {
                "tags": [
                    "Templates"
                ],
                "operationId": "updateTemplate",
                "summary": "Replace a template",
                "description": "Replaces the whole definition. Built-in templates (`engine: blade`) cannot be edited — you get a 404. Without `editor_model`, a template made in the visual editor switches to code mode (`editor: code`).",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Saved."
                    },
                    "404": {
                        "description": "No such template, or it is built-in."
                    }
                }
            },
            "delete": {
                "tags": [
                    "Templates"
                ],
                "operationId": "deleteTemplate",
                "summary": "Delete a template",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "204": {
                        "description": "Deleted."
                    }
                }
            }
        },
        "/templates/library": {
            "get": {
                "tags": [
                    "Templates"
                ],
                "operationId": "listLibraryTemplates",
                "summary": "Built-in template library",
                "description": "Ready-made templates every account can render by slug, with no setup: `\"template\": \"job-posting\"`. A template on your account with the same slug always wins. Each entry lists its `fields` (all optional; `accent` and `theme` work everywhere), `images`, `defaults`, `example` values, a `preview_url` (a real render) and the `gallery_url` page.",
                "responses": {
                    "200": {
                        "description": "The library, in gallery order.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "data": [
                                        {
                                            "slug": "job-posting",
                                            "name": "Job Posting",
                                            "width": 1200,
                                            "height": 630,
                                            "engine": "blade",
                                            "editable": false,
                                            "fields": [
                                                "title",
                                                "company",
                                                "location",
                                                "salary",
                                                "type",
                                                "eyebrow",
                                                "cta",
                                                "site",
                                                "logo",
                                                "accent",
                                                "theme"
                                            ],
                                            "images": [
                                                "logo"
                                            ],
                                            "defaults": {
                                                "title": "Job title",
                                                "eyebrow": "We're hiring",
                                                "cta": "Apply now",
                                                "accent": "#10b981"
                                            },
                                            "example": {
                                                "title": "Senior Backend Engineer",
                                                "company": "Northwind Labs",
                                                "salary": "€90k – €120k"
                                            },
                                            "preview_url": "https://shareglint.com/img/templates/job-posting.webp",
                                            "gallery_url": "https://shareglint.com/en/templates/job-posting"
                                        }
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/templates/library/{slug}/copy": {
            "post": {
                "tags": [
                    "Templates"
                ],
                "operationId": "copyLibraryTemplate",
                "summary": "Copy a built-in template into your account",
                "description": "Built-in templates are read-only. This creates an ordinary, editable HTML template (`engine: html`) from one of them, with the same fields — edit it afterwards with `PATCH /templates/{id}`. Fields the built-in reads in CSS (`accent`, `theme`, `accent_b`) are fixed to their default values in the copy. Without `slug`, the copy takes the library slug if it is free on your account — so existing calls switch to your version — and otherwise gets a `-copy` suffix.",
                "parameters": [
                    {
                        "name": "slug",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Library slug, e.g. `job-posting`."
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "example": {
                                "slug": "careers-card",
                                "name": "Careers card"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "The new template.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Template"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No built-in template with this slug."
                    },
                    "422": {
                        "description": "`slug` is taken on this account or malformed."
                    }
                }
            }
        },
        "/templates/preview": {
            "post": {
                "tags": [
                    "Templates"
                ],
                "operationId": "previewTemplate",
                "summary": "Preview without using quota",
                "description": "Renders a saved template (`template`) **or a draft** you have not saved yet (`html`, `css`, `fields`). Creates no history entry and **uses no quota** — so refining a template costs nothing. Its own rate limit: 30 calls per minute.",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "example": {
                                "fields": [
                                    "title"
                                ],
                                "html": "<h1>{{ title }}</h1>",
                                "css": "body{background:#0f172a;color:#fff;font-family:Roboto}",
                                "modifications": {
                                    "title": "Preview"
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Preview URL.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "url": "…/storage/previews/…png"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/templates/generate": {
            "post": {
                "tags": [
                    "Templates"
                ],
                "operationId": "generateTemplate",
                "summary": "Generate a template with AI",
                "description": "Paid plans only. Designs a 1200×630 template from a text `description` and/or a screenshot of a design (`image`, multipart, PNG/JPEG/WebP up to 4 MB). The answer passes the same validation and sanitiser as `POST /templates`, comes with a free preview, and is **not saved** — send `template` to `POST /templates` unchanged to keep it. Daily limits: Starter 20, Pro 50, Scale 100 (UTC day). Errors: `403 plan_required`, `429 ai_daily_limit`, `502 ai_invalid_output` (with `errors`), `502 ai_failed` (not counted), `503 ai_unavailable`. Own rate limit: 6 calls per minute.",
                "requestBody": {
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "description": {
                                        "type": "string",
                                        "maxLength": 2000,
                                        "description": "What the image should look like. Required without `image`."
                                    },
                                    "image": {
                                        "type": "string",
                                        "format": "binary",
                                        "description": "Reference design. Required without `description`."
                                    }
                                }
                            }
                        },
                        "application/json": {
                            "example": {
                                "description": "Dark blog card, big white title, author at the bottom, purple accent"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Proposed template, preview and today's usage.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "template": {
                                        "name": "Dark blog card",
                                        "width": 1200,
                                        "height": 630,
                                        "html": "<div class=\"card\"><div class=\"fitbox\"><h1 data-autofit>{{ title }}</h1></div><p>{{ author }}</p></div>",
                                        "css": ".card{display:flex;flex-direction:column;height:100%;padding:72px;background:#0b1020;color:#fff}",
                                        "fields": [
                                            "title",
                                            "author"
                                        ],
                                        "images": [],
                                        "defaults": {
                                            "title": "Ten things that ruin your sleep",
                                            "author": "Ada Lovelace"
                                        }
                                    },
                                    "preview": {
                                        "url": "…/storage/previews/…png"
                                    },
                                    "usage": {
                                        "used_today": 3,
                                        "daily_limit": 50,
                                        "remaining_today": 47
                                    }
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Free plan: `plan_required`."
                    },
                    "422": {
                        "description": "Neither `description` nor a valid `image`."
                    },
                    "429": {
                        "description": "Daily AI limit reached: `ai_daily_limit` with `resets_at`."
                    },
                    "502": {
                        "description": "`ai_invalid_output` or `ai_failed`."
                    },
                    "503": {
                        "description": "`ai_unavailable`: the feature is not enabled."
                    }
                }
            }
        },
        "/fonts": {
            "get": {
                "tags": [
                    "Templates"
                ],
                "operationId": "listFonts",
                "summary": "Available fonts",
                "description": "Families installed in our renderer. You cannot upload your own — the render page content security policy blocks external addresses.",
                "responses": {
                    "200": {
                        "description": "Available font families.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "data": [
                                        "Roboto",
                                        "Open Sans",
                                        "Carlito",
                                        "DejaVu Sans",
                                        "DejaVu Serif",
                                        "DejaVu Sans Mono",
                                        "Ubuntu",
                                        "Noto Sans CJK JP",
                                        "Noto Sans CJK SC",
                                        "Noto Color Emoji"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/account/usage": {
            "get": {
                "tags": [
                    "Account"
                ],
                "operationId": "getUsage",
                "summary": "Quota usage",
                "description": "`remaining` is what is left of this month's plan quota. `credits` is the balance of purchased top-up packs (valid 12 months, used only after the monthly quota runs out). Annual plans renew the quota monthly too.",
                "responses": {
                    "200": {
                        "description": "The current period.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "plan": "free",
                                    "period": {
                                        "start": "2026-08-01 00:00:00",
                                        "end": "2026-08-31 23:59:59"
                                    },
                                    "quota": 100,
                                    "used": 80,
                                    "remaining": 20,
                                    "credits": 0
                                }
                            }
                        }
                    }
                }
            }
        },
        "/analytics": {
            "get": {
                "tags": [
                    "Analytics"
                ],
                "operationId": "getAnalytics",
                "summary": "View analytics",
                "description": "Daily counts of images we served through a URL (`GET /i/…`; auto-OG and screenshot URLs count too), by the platform that fetched them, worked out from the User-Agent: `facebook`, `linkedin`, `x`, `slack`, `discord`, `whatsapp`, `telegram`, `imessage`, `google`, `other-bot`, `browser`. Link previews (Facebook, LinkedIn, X, Slack, Discord, WhatsApp, Telegram, iMessage) are `share: true` and add up to `shares` — roughly \"your link was pasted N times\". We store no IP address and no User-Agent, only the counters. Files fetched straight from storage (`url` of a render) are not counted; the `/i/` redirect is cached for 1 hour so crawlers come back through us. Days without views are present with zeros. Times are UTC.",
                "parameters": [
                    {
                        "name": "days",
                        "in": "query",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 90,
                            "default": 30
                        },
                        "description": "How many days back, today included."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Counters for the period.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "days": 30,
                                    "from": "2026-09-25",
                                    "to": "2026-10-24",
                                    "total": 412,
                                    "shares": 298,
                                    "sources": [
                                        {
                                            "source": "linkedin",
                                            "views": 140,
                                            "share": true
                                        },
                                        {
                                            "source": "x",
                                            "views": 96,
                                            "share": true
                                        },
                                        {
                                            "source": "google",
                                            "views": 70,
                                            "share": false
                                        },
                                        {
                                            "source": "…",
                                            "views": 0,
                                            "share": false
                                        }
                                    ],
                                    "kinds": {
                                        "image": 412,
                                        "auto_og": 0,
                                        "screenshot": 0
                                    },
                                    "daily": [
                                        {
                                            "day": "2026-09-25",
                                            "views": 12,
                                            "sources": {
                                                "facebook": 0,
                                                "linkedin": 9,
                                                "x": 3,
                                                "…": 0
                                            }
                                        }
                                    ],
                                    "templates": [
                                        {
                                            "template": "blog-og",
                                            "kind": "image",
                                            "views": 380
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "`days` outside 1–90.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/account": {
            "get": {
                "tags": [
                    "Account"
                ],
                "operationId": "getAccount",
                "summary": "Account details",
                "description": "Includes `webhook_secret` — the same one we sign the `X-Webhook-Signature` header with — and `signing_secret`, which you sign image URLs (`GET /i/{account}/{template}.{ext}`) with. Unlike an API key both are symmetric: both sides need the same value, so they can be read back at any time.",
                "responses": {
                    "200": {
                        "description": "The account.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "id": "acc_01m08s3ez1zcv28h335g9923bw",
                                    "email": "you@example.com",
                                    "name": null,
                                    "plan": "free",
                                    "webhook_secret": "whsec_...",
                                    "signing_secret": "sign_...",
                                    "created_at": "2026-08-01 12:00:00"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/account/webhook-secret": {
            "post": {
                "tags": [
                    "Account"
                ],
                "operationId": "rotateWebhookSecret",
                "summary": "Rotate the webhook secret",
                "description": "Rotation takes effect **immediately, with no grace period**: the old secret stops being valid at once, so until you paste the new one on your side you will reject our calls. We retry five times with growing backoff, so the event is not lost. This route deliberately ignores `Idempotency-Key` — the response contains a plaintext secret.",
                "responses": {
                    "200": {
                        "description": "The new secret.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "webhook_secret": "whsec_..."
                                }
                            }
                        }
                    }
                }
            }
        },
        "/account/signing-secret": {
            "post": {
                "tags": [
                    "Account"
                ],
                "operationId": "rotateSigningSecret",
                "summary": "Rotate the image URL signing secret",
                "description": "Every signed image URL issued so far stops working **immediately** — including the ones already published in `og:image` tags. That is the point: a leaked secret lets anyone render on your quota. Redirects already cached by crawlers and CDNs may live up to 1 hour. This route deliberately ignores `Idempotency-Key` — the response contains a plaintext secret.",
                "responses": {
                    "200": {
                        "description": "The new secret.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "signing_secret": "sign_..."
                                }
                            }
                        }
                    }
                }
            }
        },
        "/i/{account}/{template}.{ext}": {
            "get": {
                "tags": [
                    "Images"
                ],
                "operationId": "getSignedImage",
                "summary": "Signed image URL (for og:image)",
                "servers": [
                    {
                        "url": "https://shareglint.com"
                    }
                ],
                "security": [],
                "description": "A URL you paste straight into `<meta property=\"og:image\">` — no server code, no API key. Template fields go in the query string, `s` is an HMAC signature made with your `signing_secret`. The extension picks the file format: `.png`, `.jpg` (quality 85), `.webp` (quality 85) or `.pdf`; rasters are 2× the template size.\n\nSignature: build the canonical string `/i/{account}/{template}.{ext}` (the extension you request, e.g. `/i/acc_…/blog-og.jpg`) + `?` + every query parameter except `s`, sorted by name (byte order), each name and value percent-encoded per RFC 3986 (space = `%20`), joined as `name=value` with `&` (the `?` stays even with no parameters). `s` = the first 32 lowercase hex characters of HMAC-SHA256(canonical, signing_secret). The URL itself may use any order and encoding (`+` or `%20`) — we decode it and rebuild the canonical string.\n\nAn identical render that already exists is a 302 with no quota used and no rendering. Otherwise we render synchronously, use one render of quota exactly like `POST /images`, and redirect. Redirects carry `Cache-Control: public` for up to 1 hour (never past the file's retention; the file itself is cached for a year). The short redirect lifetime is deliberate: crawlers come back through us, so every served image is counted in `GET /analytics`. Errors are short `text/plain` bodies, never HTML. Limits: 120 correctly signed requests per minute per account, 600 requests per minute per IP.",
                "parameters": [
                    {
                        "name": "account",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Your account `id` from `GET /account` (`acc_…`). Public, not a credential."
                    },
                    {
                        "name": "template",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Template slug, e.g. `blog-og`."
                    },
                    {
                        "name": "ext",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "enum": [
                                "png",
                                "jpg",
                                "webp",
                                "pdf"
                            ]
                        },
                        "description": "File format. Part of the signed path: a signature for `.png` does not open `.pdf`."
                    },
                    {
                        "name": "s",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9a-f]{32}$"
                        },
                        "description": "Signature — see above."
                    },
                    {
                        "name": "locale",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "en",
                                "pl",
                                "de",
                                "es",
                                "fr"
                            ]
                        },
                        "description": "Language for text the template adds itself. Signed like any other parameter."
                    },
                    {
                        "name": "{field}",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Any field the template declares (`fields`, `images`, `defaults`; built-in templates: their documented fields plus `accent` and `theme`). An undeclared field is a 400."
                    }
                ],
                "responses": {
                    "302": {
                        "description": "Redirect to the finished file in the requested format."
                    },
                    "400": {
                        "description": "A field the template does not declare, an unsupported `locale`, or a non-string parameter."
                    },
                    "402": {
                        "description": "Monthly quota exhausted (only when a new render was needed)."
                    },
                    "403": {
                        "description": "Missing, malformed or invalid signature."
                    },
                    "404": {
                        "description": "No such account or template. The two are indistinguishable."
                    },
                    "429": {
                        "description": "Rate limit. See `Retry-After`."
                    },
                    "500": {
                        "description": "The render failed. No quota is used."
                    },
                    "503": {
                        "description": "The render did not finish in time. It keeps going — retry after `Retry-After`."
                    }
                }
            }
        },
        "/auto-og/sites": {
            "get": {
                "tags": [
                    "Auto OG"
                ],
                "operationId": "listAutoOgSites",
                "summary": "List allowed domains",
                "description": "Domains whose pages get auto OG images (`GET /u/{account}.{ext}?url=…`). Each entry covers the domain, `www.` and every subdomain. `url_prefix` is your image URL up to `?url=` — append the URL-encoded page address.",
                "responses": {
                    "200": {
                        "description": "Allowed domains, alphabetically.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "data": [
                                        {
                                            "id": "aog_01m08s3ez1zcv28h335g9923bw",
                                            "host": "example.com",
                                            "template": "site-card",
                                            "accent": "#2563eb",
                                            "theme": null,
                                            "keep_query": false,
                                            "created_at": "2026-10-24 12:00:00"
                                        }
                                    ],
                                    "url_prefix": "https://shareglint.com/u/acc_01m08s3ez1zcv28h335g9923bw.png?url="
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "tags": [
                    "Auto OG"
                ],
                "operationId": "createAutoOgSite",
                "summary": "Allow a domain",
                "description": "`host` may be a domain or a whole URL; it is stored lower-case without `www.` (`https://www.Example.com/blog` → `example.com`). IP addresses, single-label names and our own domain are rejected. At most 50 domains per account. `template` defaults to `site-card`; any template on your account works and gets the fields it declares out of `title`, `description`, `site`, `url`, `favicon`, `image`, `accent`, `theme`. `accent` (`#rrggbb`) and `theme` (`dark`/`light`) override the page; `null` = the page's `theme-color` (when it is a real colour) and the template default. `keep_query`: keep the page's query string (default `false`: dropped).",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "host"
                                ],
                                "properties": {
                                    "host": {
                                        "type": "string",
                                        "example": "example.com"
                                    },
                                    "template": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "example": "site-card"
                                    },
                                    "accent": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "pattern": "^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$"
                                    },
                                    "theme": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "enum": [
                                            "dark",
                                            "light",
                                            null
                                        ]
                                    },
                                    "keep_query": {
                                        "type": "boolean",
                                        "default": false
                                    }
                                }
                            },
                            "example": {
                                "host": "example.com",
                                "template": "site-card",
                                "accent": "#2563eb",
                                "theme": "light"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "The new entry.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "id": "aog_01m08s3ez1zcv28h335g9923bw",
                                    "host": "example.com",
                                    "template": "site-card",
                                    "accent": "#2563eb",
                                    "theme": null,
                                    "keep_query": false,
                                    "created_at": "2026-10-24 12:00:00"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Invalid or duplicate `host`, unknown `template`, bad `accent`, or the domain limit reached.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/auto-og/sites/{id}": {
            "patch": {
                "tags": [
                    "Auto OG"
                ],
                "operationId": "updateAutoOgSite",
                "summary": "Change a domain",
                "description": "Send only what changes (same fields as creating). `accent: null` / `theme: null` go back to the page colour and the template default. Published `/u/` URLs stay the same; the next crawler request gets a card with the new settings.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "`aog_…`"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The updated entry.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "id": "aog_01m08s3ez1zcv28h335g9923bw",
                                    "host": "example.com",
                                    "template": "site-card",
                                    "accent": "#2563eb",
                                    "theme": null,
                                    "keep_query": false,
                                    "created_at": "2026-10-24 12:00:00"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No such domain on this account."
                    },
                    "422": {
                        "description": "Validation failed (see creating)."
                    }
                }
            },
            "delete": {
                "tags": [
                    "Auto OG"
                ],
                "operationId": "deleteAutoOgSite",
                "summary": "Remove a domain",
                "description": "From now on `/u/` URLs of its pages answer `403`. Images already rendered stay in your history until retention ends.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "204": {
                        "description": "Removed."
                    },
                    "404": {
                        "description": "No such domain on this account."
                    }
                }
            }
        },
        "/u/{account}.{ext}": {
            "get": {
                "tags": [
                    "Auto OG"
                ],
                "operationId": "getAutoOgImage",
                "summary": "Auto OG image of a page (for og:image)",
                "servers": [
                    {
                        "url": "https://shareglint.com"
                    }
                ],
                "security": [],
                "description": "One tag for a whole site: `<meta property=\"og:image\" content=\"https://shareglint.com/u/acc_…png?url=PAGE\">`. No API key and no signature — only pages whose host is on your allowed domains (`/auto-og/sites`) get images, any other host is a `403`.\n\nWe fetch the page (at most 5 s and 1.5 MB, private addresses and redirects off your domain refused), read its title, description, site name, icon, `og:image`, `theme-color` and canonical URL, and render a card with the domain's template. The query string is dropped unless the domain has `keep_query`; `utm_*`, `fbclid`, `gclid` and similar always are. What we read from a page is kept for 60 minutes. A page that cannot be read (error status, timeout, not HTML) gets a card with just the site name — never a broken image.\n\nCaching and quota work like `GET /i/…`: the image is cached by the fields on the card, so an unchanged page is a `302` with no quota used, whatever address or tracking parameters it is shared with. A new card uses one render. Errors are short `text/plain` bodies. Limits: 120 requests per minute per account, 600 per IP.",
                "parameters": [
                    {
                        "name": "account",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Your account `id` (`acc_…`). Public, not a credential."
                    },
                    {
                        "name": "ext",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "enum": [
                                "png",
                                "jpg",
                                "webp"
                            ]
                        },
                        "description": "File format."
                    },
                    {
                        "name": "url",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "format": "uri"
                        },
                        "description": "Absolute http(s) address of the page, URL-encoded."
                    }
                ],
                "responses": {
                    "302": {
                        "description": "Redirect to the finished image."
                    },
                    "400": {
                        "description": "`url` missing or not an absolute http(s) address."
                    },
                    "402": {
                        "description": "Quota exhausted (only when a new card was needed)."
                    },
                    "403": {
                        "description": "The page's domain is not on your allowed domains."
                    },
                    "404": {
                        "description": "No such account, or the domain's template was deleted."
                    },
                    "429": {
                        "description": "Rate limit. See `Retry-After`."
                    },
                    "500": {
                        "description": "The render failed. No quota is used."
                    },
                    "503": {
                        "description": "The render did not finish in time. It keeps going — retry after `Retry-After`."
                    }
                }
            }
        },
        "/screenshots": {
            "post": {
                "tags": [
                    "Screenshots"
                ],
                "operationId": "createScreenshot",
                "summary": "Take a screenshot of a web page",
                "description": "Asynchronous by default, exactly like `POST /images`: 202 with an `id`, then `GET /screenshots/{id}` (or `GET /images/{id}` — screenshots share the render history). With `?wait=true` we hold the connection for up to 25 s and return 200, or 503 `sync_overloaded` with the `id`.\n\nPricing: a viewport screenshot uses 1 render of quota, a full-page one 2. The same URL with the same options within 24 h returns the existing file and uses nothing. A page that fails (4xx/5xx status, timeout, blocked address) gives `status: failed` with a reason in `error`, and the quota is returned. On the Free plan the screenshot carries the same small badge as images (bottom right; on a full page, at the bottom of the page).\n\nThe browser runs in an isolated network and reaches the internet only through an egress proxy that allows public addresses on ports 80 and 443 — redirects, frames and subresources pointing at private, loopback, link-local or metadata addresses are refused. Webhooks for screenshots carry the events `screenshot.completed` / `screenshot.failed`.",
                "parameters": [
                    {
                        "name": "wait",
                        "in": "query",
                        "schema": {
                            "type": "boolean"
                        },
                        "description": "Wait for the result instead of polling."
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "schema": {
                            "type": "string"
                        },
                        "description": "The same key with the same body returns the same response, with no second screenshot."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "url"
                                ],
                                "properties": {
                                    "url": {
                                        "type": "string",
                                        "format": "uri",
                                        "description": "Public http(s) address of the page. Private, loopback and internal addresses are refused (422) — and so is anything the page redirects or links to that is not public, on ports other than 80/443.",
                                        "example": "https://example.com"
                                    },
                                    "format": {
                                        "type": "string",
                                        "enum": [
                                            "png",
                                            "jpeg",
                                            "jpg",
                                            "webp",
                                            "pdf"
                                        ],
                                        "default": "png",
                                        "description": "`pdf`: one page the size of the viewport (full page: as many viewport-sized pages as needed), backgrounds printed, text kept as vector."
                                    },
                                    "quality": {
                                        "type": "integer",
                                        "minimum": 1,
                                        "maximum": 100,
                                        "default": 85,
                                        "description": "For `jpeg`/`webp` only."
                                    },
                                    "scale": {
                                        "type": "integer",
                                        "enum": [
                                            1,
                                            2
                                        ],
                                        "default": 1,
                                        "description": "Device pixel ratio for rasters: 2 = retina (twice the pixels). Ignored for `pdf`."
                                    },
                                    "viewport_width": {
                                        "type": "integer",
                                        "minimum": 320,
                                        "maximum": 3840,
                                        "default": 1280
                                    },
                                    "viewport_height": {
                                        "type": "integer",
                                        "minimum": 200,
                                        "maximum": 2160,
                                        "default": 800
                                    },
                                    "device": {
                                        "type": "string",
                                        "enum": [
                                            "desktop",
                                            "laptop",
                                            "tablet",
                                            "mobile"
                                        ],
                                        "description": "Preset viewport; `tablet` and `mobile` also emulate a touch device with a mobile user agent. Overrides `viewport_width`/`viewport_height`."
                                    },
                                    "full_page": {
                                        "type": "boolean",
                                        "default": false,
                                        "description": "The whole scrollable page instead of the viewport, up to 16,000 CSS px tall. Costs 2 renders instead of 1."
                                    },
                                    "dark_mode": {
                                        "type": "boolean",
                                        "default": false,
                                        "description": "Emulate `prefers-color-scheme: dark`."
                                    },
                                    "delay": {
                                        "type": "integer",
                                        "minimum": 0,
                                        "maximum": 10,
                                        "default": 0,
                                        "description": "Extra seconds to wait after the page has loaded (animations, lazy content)."
                                    },
                                    "wait_for": {
                                        "type": "string",
                                        "maxLength": 200,
                                        "description": "CSS selector to wait for before capturing (within the 20 s page limit)."
                                    },
                                    "hide": {
                                        "type": "array",
                                        "maxItems": 20,
                                        "items": {
                                            "type": "string",
                                            "maxLength": 200
                                        },
                                        "description": "CSS selectors to hide (chat widgets, promos). Plain selectors: no `{`, `}`, `;` or `<`. In `GET /s/…`: one comma-separated value."
                                    },
                                    "block_ads": {
                                        "type": "boolean",
                                        "default": false,
                                        "description": "Block common ad networks and hide the usual cookie-consent banners (a small built-in list, not a full ad blocker)."
                                    },
                                    "webhook_url": {
                                        "type": "string",
                                        "description": "We call this URL when the screenshot finishes. Private-network addresses are rejected with a 422."
                                    }
                                }
                            },
                            "example": {
                                "url": "https://example.com",
                                "full_page": true,
                                "format": "webp",
                                "block_ads": true
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Finished (only with `?wait=true`).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Render"
                                }
                            }
                        }
                    },
                    "202": {
                        "description": "Accepted into the queue.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "id": "img_01m08…",
                                    "kind": "screenshot",
                                    "status": "queued",
                                    "format": "png",
                                    "url": "…/v1/screenshots/img_01m08…"
                                }
                            }
                        }
                    },
                    "402": {
                        "description": "Not enough quota for this screenshot (`quota_exceeded`).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation error: missing or non-public `url`, viewport out of range, unknown `device`, `delay` too long, an unsafe selector."
                    },
                    "500": {
                        "description": "With `?wait=true`: the page could not be captured (`status: failed`, reason in `error`). No quota is used."
                    },
                    "503": {
                        "description": "Sync mode ran out of time (`sync_overloaded`). The body carries `id`."
                    }
                }
            }
        },
        "/screenshots/{id}": {
            "get": {
                "tags": [
                    "Screenshots"
                ],
                "operationId": "getScreenshot",
                "summary": "Screenshot status",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The screenshot (`kind: screenshot`).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Render"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No such render on this account."
                    }
                }
            }
        },
        "/s/{account}.{ext}": {
            "get": {
                "tags": [
                    "Screenshots"
                ],
                "operationId": "getSignedScreenshot",
                "summary": "Signed screenshot URL (for <img> or og:image)",
                "servers": [
                    {
                        "url": "https://shareglint.com"
                    }
                ],
                "security": [],
                "description": "A screenshot as a plain URL, no API key: `/s/{account}.png?url=https%3A%2F%2Fexample.com&full_page=true&s=SIGNATURE`. Options are the `POST /screenshots` fields as query parameters (`hide` comma-separated); the extension picks the format. The signature is the same as for `GET /i/…` with the path `/s/{account}.{ext}`: canonical string = path + `?` + every parameter except `s`, sorted by name, RFC 3986-encoded, joined with `&`; `s` = first 32 hex chars of HMAC-SHA256(canonical, signing_secret).\n\nA screenshot of the same URL and options from the last 24 h is a 302 with no quota used. Otherwise we capture synchronously (up to 25 s), charge like `POST /screenshots` and redirect. Redirects are cached for up to 1 hour; errors are short `text/plain` bodies. Same rate limits as `GET /i/…`.",
                "parameters": [
                    {
                        "name": "account",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Your account `id` (`acc_…`). Public, not a credential."
                    },
                    {
                        "name": "ext",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "enum": [
                                "png",
                                "jpg",
                                "webp",
                                "pdf"
                            ]
                        },
                        "description": "File format. Part of the signed path."
                    },
                    {
                        "name": "url",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "format": "uri"
                        },
                        "description": "The page, URL-encoded."
                    },
                    {
                        "name": "s",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9a-f]{32}$"
                        },
                        "description": "Signature — see above."
                    },
                    {
                        "name": "{option}",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Any `POST /screenshots` option: `full_page`, `viewport_width`, `viewport_height`, `device`, `dark_mode`, `delay`, `wait_for`, `hide`, `block_ads`, `scale`, `quality`. Anything else is a 400."
                    }
                ],
                "responses": {
                    "302": {
                        "description": "Redirect to the finished file."
                    },
                    "400": {
                        "description": "Invalid or non-public `url`, an invalid option, or an unknown parameter."
                    },
                    "402": {
                        "description": "Quota exhausted (only when a new screenshot was needed)."
                    },
                    "403": {
                        "description": "Missing, malformed or invalid signature."
                    },
                    "404": {
                        "description": "No such account."
                    },
                    "429": {
                        "description": "Rate limit. See `Retry-After`."
                    },
                    "502": {
                        "description": "The page could not be captured; the reason follows. No quota is used."
                    },
                    "503": {
                        "description": "The screenshot did not finish in time. It keeps going — retry after `Retry-After`."
                    }
                }
            }
        },
        "/keys": {
            "get": {
                "tags": [
                    "Account"
                ],
                "operationId": "listApiKeys",
                "summary": "List API keys",
                "description": "We return the prefix only — the full key cannot be recovered, the database holds just its sha256.",
                "responses": {
                    "200": {
                        "description": "Keys on this account."
                    }
                }
            },
            "post": {
                "tags": [
                    "Account"
                ],
                "operationId": "createApiKey",
                "summary": "Issue an API key",
                "description": "The response carries `secret`, and this is the **only** time you will see it. This route deliberately ignores `Idempotency-Key`: storing the response would mean keeping a plaintext credential in the database.",
                "responses": {
                    "201": {
                        "description": "The new key.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "id": "key_01m08…",
                                    "secret": "sk_live_…"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/keys/{id}": {
            "delete": {
                "tags": [
                    "Account"
                ],
                "operationId": "revokeApiKey",
                "summary": "Revoke a key",
                "description": "The last active key cannot be revoked (422 `last_key`) — without a working key you could not issue a new one and would lock yourself out.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "204": {
                        "description": "Revoked."
                    },
                    "422": {
                        "description": "This is the last active key (`last_key`)."
                    }
                }
            }
        }
    }
}