{
  "openapi": "3.1.1",
  "info": {
    "title": "StackerScan Public API",
    "version": "1.0.0",
    "summary": "Open, read-only precious-metals price, premium and share data. No authentication of any kind is required.",
    "description": "The public, read-only HTTP API behind StackerScan — live and historical precious-metals spot prices, observed dealer premiums, the supported-currency catalog, and read access to shared stack images and live image galleries.\n\n## Authentication\n\nThis API is fully open: no authentication, no API keys, and no registration are required for any endpoint in this document.\nThere is no `securitySchemes` section and the root `security` is the empty list, which says the same thing structurally: every operation below is callable by an anonymous client over plain HTTPS. Nothing here is rate-limited by account, because there are no accounts involved.\n\nBrowsers are welcome too: every endpoint in this document answers cross-origin requests with `Access-Control-Allow-Origin: *`, so it is callable from a web page without a proxy.\n\nThe stack-share endpoints (`/api/public/stack-shares/{code}`) take a five-character **short code**. A code is a public, unlisted address rather than a secret, so those endpoints are the one place this API rate-limits: per caller, never per account, with `429` and a usable `Retry-After` when a budget is exhausted and `503` when the protection itself is unavailable. Unknown, expired, revoked and invalidated links all answer the same generic `404`; an active but empty gallery is a `200` with no images.\n\n## Errors\n\nEvery `/api/*` response that is not a success is an RFC 9457 `application/problem+json` document (#1115), including from the `.md` endpoints — a markdown endpoint serves `text/markdown` on success and `problem+json` on failure. The `type` member is a dereferenceable URL under https://www.stackerscan.com/errors, drawn from a closed vocabulary of 7 codes: `invalid-parameter`, `missing-parameter`, `unauthorized`, `not-found`, `rate-limited`, `upstream-unavailable`, `internal-error`. `title` describes the code and never varies per occurrence; `detail` describes the occurrence and names the offending parameter. `detail` and `hint` are English-only by decision: they are machine-consumer text, and human-facing copy lives in the translated UI layer keyed off the code.\n\n## Parameter validation\n\nInvalid input is rejected rather than silently replaced (#1122). A parameter outside its documented enum or bounds answers `400 invalid-parameter`; an absent required parameter answers `400 missing-parameter`. Integers are strict: `days=90abc` is rejected, not read as `90`.\n\n`currency`/`base` are the one enum this document cannot enumerate. They validate against the **live** catalog served by `/api/currencies`, which is a runtime read; when that read is unavailable the endpoints **fail open** and accept any syntactically plausible code rather than turning a cache outage into an API outage. Treat `/api/currencies` as the authority for what is accepted, and expect the enum to be unenforced in the window where the catalog cannot be read.\n\n## A naming trap worth reading twice\n\n`/api/metal-prices` (no suffix) is **private** and is not in this document: it answers `401 unauthorized` to an anonymous caller. `/api/metal-prices.md` is public, is described below, and reads an entirely different data path — it is not a markdown rendering of the private endpoint. The two are unrelated endpoints that happen to share a prefix. The public JSON equivalent you probably want is `/api/premiums/metal-prices`.\n\nSimilarly, `/api/deployment-info` and `/api/credits/packages` answer without a credential but are deliberately absent from this document (#1114): they are a build diagnostic and checkout plumbing, not published promises. Anything not listed under `paths` is not part of the public contract, whatever it currently answers.",
    "license": {
      "name": "StackerScan API Terms",
      "url": "https://www.stackerscan.com/terms"
    },
    "contact": {
      "name": "StackerScan support",
      "url": "https://www.stackerscan.com/contact",
      "email": "support@stackerscan.com"
    }
  },
  "servers": [
    {
      "url": "https://www.stackerscan.com",
      "description": "Production. The apex permanently redirects here."
    }
  ],
  "security": [],
  "externalDocs": {
    "url": "https://www.stackerscan.com/errors",
    "description": "The error-code vocabulary every failure response references."
  },
  "tags": [
    {
      "name": "Prices",
      "description": "Spot prices, current and historical."
    },
    {
      "name": "Charts",
      "description": "Daily series for charting."
    },
    {
      "name": "Premiums",
      "description": "Observed premiums over spot, and the Goldback multiple."
    },
    {
      "name": "Markdown",
      "description": "The same data as markdown documents, for agents and crawlers."
    },
    {
      "name": "Reference",
      "description": "The currency catalog other parameters validate against."
    },
    {
      "name": "Sharing",
      "description": "Read access to a published share: a stack image or live image gallery by short code."
    },
    {
      "name": "Diagnostics",
      "description": "Liveness."
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Liveness check",
        "description": "Reports whether the web tier is serving. Uncached — the response carries `no-store`. Intended for monitoring rather than as a data endpoint.",
        "tags": [
          "Diagnostics"
        ],
        "responses": {
          "200": {
            "description": "The service is serving requests.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "timestamp",
                    "service",
                    "version"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "healthy"
                      ]
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "service": {
                      "type": "string"
                    },
                    "version": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The health check itself failed; the body carries a `health` extension member.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/PlatformRejected"
          }
        }
      }
    },
    "/api/currencies": {
      "get": {
        "operationId": "listCurrencies",
        "summary": "List supported currencies",
        "description": "The live catalog of codes accepted by `currency`/`base` everywhere else in this document — ISO 4217 fiat plus `BTC` and the metal codes. Synced daily. This endpoint is the authority for that enum, which is why the enum is not restated in the parameter schemas.",
        "tags": [
          "Reference"
        ],
        "responses": {
          "200": {
            "description": "The catalog, ordered by code.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": [
                      "code",
                      "name"
                    ],
                    "properties": {
                      "code": {
                        "type": "string",
                        "examples": [
                          "USD",
                          "XAU"
                        ]
                      },
                      "name": {
                        "type": "string",
                        "examples": [
                          "United States Dollar"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The catalog is temporarily unreadable. Unlike the endpoints that merely validate against it, this one has nothing to fail open to — its payload *is* the catalog.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/PlatformRejected"
          }
        }
      }
    },
    "/api/currencies.md": {
      "get": {
        "operationId": "getCurrenciesMarkdown",
        "summary": "Supported currencies, as markdown",
        "description": "The same catalog as `/api/currencies`, rendered as a markdown document for agents and crawlers. Failures are still `problem+json`, not markdown.",
        "tags": [
          "Markdown"
        ],
        "responses": {
          "200": {
            "description": "The catalog as a markdown list.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "The catalog is temporarily unreadable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/PlatformRejected"
          }
        }
      }
    },
    "/api/metal-prices.md": {
      "get": {
        "operationId": "getMetalPricesMarkdown",
        "summary": "Metal spot prices, as markdown",
        "description": "Current spot prices and recent history for gold, silver, platinum, palladium, copper and nickel, as markdown tables. **Not** a rendering of the private `/api/metal-prices`: despite the name, the two are unrelated endpoints reading different data paths, and only this one is public.",
        "tags": [
          "Markdown",
          "Prices"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Currency"
          },
          {
            "$ref": "#/components/parameters/Unit"
          }
        ],
        "responses": {
          "200": {
            "description": "Spot price tables in the requested currency and unit.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/api/metal-premiums.md": {
      "get": {
        "operationId": "getMetalPremiumsMarkdown",
        "summary": "Observed premiums over spot, as markdown",
        "description": "Rolling fine-metal-weighted median asking premiums over spot, by category and product, as markdown tables. `metal` is required. Covers silver, gold, platinum, palladium, copper, nickel and Goldback. Goldback percentages use the Goldback reference rate rather than raw contained-gold spot and are excluded from ordinary gold statistics.",
        "tags": [
          "Markdown",
          "Premiums"
        ],
        "parameters": [
          {
            "name": "metal",
            "in": "query",
            "description": "Which premium dataset to render. Required — omitting it answers `400 missing-parameter` rather than choosing for you. Matched case-insensitively.",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "silver",
                "gold",
                "platinum",
                "palladium",
                "copper",
                "nickel",
                "goldback"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The premium tables for the requested metal. When the dataset is temporarily unavailable this is still a 200 whose body says so.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/api/how-to-use.md": {
      "get": {
        "operationId": "getHowToUseMarkdown",
        "summary": "Product guide, as markdown",
        "description": "A step-by-step guide to using StackerScan — accounts, adding items, receipt scanning, photos, portfolio management, tags and credits. Prose about the product, not API data.",
        "tags": [
          "Markdown"
        ],
        "responses": {
          "200": {
            "description": "The guide as a markdown document.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/PlatformRejected"
          }
        }
      }
    },
    "/api/premiums/metal-prices": {
      "get": {
        "operationId": "getPremiumCalculatorMetalPrices",
        "summary": "Current spot prices as JSON",
        "description": "Current spot prices in the requested currency and unit — the JSON endpoint most callers reaching for the private `/api/metal-prices` actually want. Note that `metals` here is filtered by metal **symbol** (`xau`), not name (`gold`), unlike `/api/metal-prices-previous-day`; that difference is real and long-standing rather than a documentation slip.",
        "tags": [
          "Prices"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BaseCurrency"
          },
          {
            "$ref": "#/components/parameters/Unit"
          },
          {
            "name": "metals",
            "in": "query",
            "description": "Comma-separated metal **symbols** to include. Omit for every metal. Matched case-insensitively; duplicates collapse.",
            "required": false,
            "schema": {
              "type": "string",
              "examples": [
                "xau,xag"
              ],
              "pattern": "^\\s*(xau|xag|xpt|xpd|xcu|ni)(\\s*,\\s*(xau|xag|xpt|xpd|xcu|ni))*\\s*$"
            }
          },
          {
            "name": "analysisId",
            "in": "query",
            "description": "Optional opaque correlation identifier echoed through to the pricing backend. Bounded to 128 characters of `A-Za-z0-9._~:-`.",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9._~:-]{1,128}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Spot prices for the requested metals.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "Per-metal current prices with the base currency and unit they are quoted in."
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/premiums/goldback-rate": {
      "get": {
        "operationId": "getGoldbackRate",
        "summary": "Latest Goldback premium multiple",
        "description": "The most recently published Goldback exchange rate, expressed as a multiple of gold spot. Goldback prices elsewhere in this API are derived from gold spot and this multiple rather than quoted directly.",
        "tags": [
          "Premiums"
        ],
        "responses": {
          "200": {
            "description": "The latest published multiple and the date it is for.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "premiumMultiple",
                    "rateDate"
                  ],
                  "properties": {
                    "premiumMultiple": {
                      "type": "number"
                    },
                    "rateDate": {
                      "type": "string",
                      "format": "date"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "4XX": {
            "$ref": "#/components/responses/PlatformRejected"
          }
        }
      }
    },
    "/api/metal-prices-previous-day": {
      "get": {
        "operationId": "getPreviousDayMetalPrices",
        "summary": "Previous session's closing prices",
        "description": "The most recent close before the cutoff date for each requested metal, used to compute day-over-day change. Looks back up to a week so a weekend or holiday still yields a price. Metals are named here (`gold`), not symbolled (`xau`).",
        "tags": [
          "Prices"
        ],
        "parameters": [
          {
            "name": "metals",
            "in": "query",
            "description": "Comma-separated metal **names**. Defaults to the six traded metals; `goldback` is accepted and priced from the gold series. Matched case-insensitively.",
            "required": false,
            "schema": {
              "type": "string",
              "default": "gold,silver,platinum,palladium,copper,nickel",
              "pattern": "^\\s*(gold|silver|platinum|palladium|copper|nickel|goldback)(\\s*,\\s*(gold|silver|platinum|palladium|copper|nickel|goldback))*\\s*$"
            }
          },
          {
            "$ref": "#/components/parameters/BaseCurrency"
          },
          {
            "name": "excludeDate",
            "in": "query",
            "description": "The quote date to look *before*, as `YYYY-MM-DD`. Defaults to today. Must be a real calendar date — `2025-02-31` is rejected.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date",
              "examples": [
                "2026-08-21"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Previous-close prices keyed by metal symbol.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "No price data was found for any of the requested metals.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/metal-price-charts": {
      "get": {
        "operationId": "getMetalPriceChart",
        "summary": "Historical price series for one metal",
        "description": "A daily price series for a single metal over a trailing window. `metal` is required — the endpoint used to answer for silver when the parameter was missing *or* invalid, which made the two cases indistinguishable (#1122).",
        "tags": [
          "Charts"
        ],
        "parameters": [
          {
            "name": "metal",
            "in": "query",
            "description": "The series to return. `goldback` is derived from the gold series and the published premium multiple rather than being a traded metal. Matched case-insensitively.",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "gold",
                "silver",
                "platinum",
                "palladium",
                "copper",
                "nickel",
                "goldback"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/BaseCurrency"
          },
          {
            "$ref": "#/components/parameters/Days"
          },
          {
            "$ref": "#/components/parameters/ChartFormat"
          }
        ],
        "responses": {
          "200": {
            "description": "The requested series, plus market-session metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/goldback-price-charts": {
      "get": {
        "operationId": "getGoldbackPriceChart",
        "summary": "Historical Goldback price series",
        "description": "The Goldback series over a trailing window, derived from gold spot and the published premium multiple. Equivalent to `/api/metal-price-charts?metal=goldback`, kept as its own endpoint because it also carries the multiples it was derived from.",
        "tags": [
          "Charts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BaseCurrency"
          },
          {
            "$ref": "#/components/parameters/Days"
          },
          {
            "$ref": "#/components/parameters/ChartFormat"
          }
        ],
        "responses": {
          "200": {
            "description": "The Goldback series over the requested window.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/transactions/spot-price": {
      "get": {
        "operationId": "getSpotPriceForDate",
        "summary": "Spot price for one metal on one date",
        "description": "The spot price of a single metal on a given date — the endpoint used to value a transaction at the moment it happened. `date` and `metal` are both required.",
        "tags": [
          "Prices"
        ],
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "description": "The valuation date as `YYYY-MM-DD`. Required, and must be a real calendar date. Ignored when `current=true`.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date",
              "examples": [
                "2026-08-21"
              ]
            }
          },
          {
            "name": "metal",
            "in": "query",
            "description": "Which metal to price. Required. `goldback` is not accepted here — request `metal=gold` with `formOfMetal=goldback` instead.",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "gold",
                "silver",
                "platinum",
                "palladium",
                "copper",
                "nickel"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/BaseCurrency"
          },
          {
            "$ref": "#/components/parameters/Unit"
          },
          {
            "name": "formOfMetal",
            "in": "query",
            "description": "Free-form form-of-metal label carried on the transaction. The only value that changes the answer is `goldback`, which (with `metal=gold`) derives the Goldback price from gold spot and the published multiple.",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64,
              "pattern": "^[^<>]*$"
            }
          },
          {
            "name": "current",
            "in": "query",
            "description": "Price at the latest available quote instead of on `date`. Exactly `true` or `false`.",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resolved spot price in the requested currency and unit.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "No spot price is available for that metal on that date.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/public/stack-shares/{code}": {
      "get": {
        "operationId": "getStackShare",
        "summary": "Read a short-link stack share",
        "description": "What a `/s/{code}` link shows: one transaction photo, one generated visualization, or the first page of a live photo or visualization gallery. Galleries are live — they list the section's current images at request time, not a snapshot. Every response is `Cache-Control: no-store`.",
        "tags": [
          "Sharing"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShareCode"
          },
          {
            "$ref": "#/components/parameters/SharePageSize"
          },
          {
            "$ref": "#/components/parameters/ShareCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "The share's public body.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "An individual share carries its one `image`; a live gallery carries the first page of `images` and a `nextCursor` (null when there are no more). Which of the two arrives is told by `kind`. A gallery whose section is currently empty is a success with an empty `images` list, not an unavailable link.",
                  "required": [
                    "kind"
                  ],
                  "properties": {
                    "kind": {
                      "type": "string",
                      "enum": [
                        "transaction-photo",
                        "visualization",
                        "photo-gallery",
                        "visualization-gallery"
                      ]
                    },
                    "image": {
                      "type": "object",
                      "required": [
                        "id",
                        "kind"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid",
                          "description": "The image id to pass to the image endpoint."
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "transaction-photo",
                            "visualization"
                          ]
                        },
                        "photoDetails": {
                          "type": "object",
                          "description": "Present on a transaction photo only when the owner enabled captions for this link; each field is omitted when the product has none.",
                          "properties": {
                            "productName": {
                              "type": "string"
                            },
                            "metal": {
                              "type": "string"
                            },
                            "formOfMetal": {
                              "type": "string"
                            },
                            "weightTroyOz": {
                              "type": "number"
                            },
                            "weightGram": {
                              "type": "number"
                            },
                            "purity": {
                              "type": "string"
                            }
                          },
                          "additionalProperties": false
                        },
                        "caption": {
                          "type": "string",
                          "description": "Present on a visualization only when the owner enabled captions for this link and wrote one."
                        }
                      },
                      "additionalProperties": false
                    },
                    "images": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "kind"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The image id to pass to the image endpoint."
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "transaction-photo",
                              "visualization"
                            ]
                          },
                          "photoDetails": {
                            "type": "object",
                            "description": "Present on a transaction photo only when the owner enabled captions for this link; each field is omitted when the product has none.",
                            "properties": {
                              "productName": {
                                "type": "string"
                              },
                              "metal": {
                                "type": "string"
                              },
                              "formOfMetal": {
                                "type": "string"
                              },
                              "weightTroyOz": {
                                "type": "number"
                              },
                              "weightGram": {
                                "type": "number"
                              },
                              "purity": {
                                "type": "string"
                              }
                            },
                            "additionalProperties": false
                          },
                          "caption": {
                            "type": "string",
                            "description": "Present on a visualization only when the owner enabled captions for this link and wrote one."
                          }
                        },
                        "additionalProperties": false
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "The link is unknown, expired, revoked or its target no longer exists — one answer for all four, with no owner or lifecycle detail. Also the answer for an image id that is not this share's own.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "A per-caller budget is exhausted: distinct codes tried, or requests of this kind, within a rolling window. `Retry-After` says when the exhausted window admits another request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "description": "The rate-limit protection or the share's authority could not be established, so the request was refused without looking anything up. The link is not invalidated; retry after `Retry-After`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/stack-shares/{code}/images": {
      "get": {
        "operationId": "listStackShareImages",
        "summary": "Read a later page of a live gallery",
        "description": "The page that `nextCursor` from the previous page names. Individual shares have no listing and answer the generic `404`. Membership is evaluated at request time, so an image removed between pages is simply absent.",
        "tags": [
          "Sharing"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShareCode"
          },
          {
            "$ref": "#/components/parameters/ShareCursor"
          },
          {
            "$ref": "#/components/parameters/SharePageSize"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the gallery.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "One page of a live gallery.",
                  "required": [
                    "kind",
                    "images",
                    "nextCursor"
                  ],
                  "properties": {
                    "kind": {
                      "type": "string",
                      "enum": [
                        "photo-gallery",
                        "visualization-gallery"
                      ]
                    },
                    "images": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "kind"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The image id to pass to the image endpoint."
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "transaction-photo",
                              "visualization"
                            ]
                          },
                          "photoDetails": {
                            "type": "object",
                            "description": "Present on a transaction photo only when the owner enabled captions for this link; each field is omitted when the product has none.",
                            "properties": {
                              "productName": {
                                "type": "string"
                              },
                              "metal": {
                                "type": "string"
                              },
                              "formOfMetal": {
                                "type": "string"
                              },
                              "weightTroyOz": {
                                "type": "number"
                              },
                              "weightGram": {
                                "type": "number"
                              },
                              "purity": {
                                "type": "string"
                              }
                            },
                            "additionalProperties": false
                          },
                          "caption": {
                            "type": "string",
                            "description": "Present on a visualization only when the owner enabled captions for this link and wrote one."
                          }
                        },
                        "additionalProperties": false
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "The link is unknown, expired, revoked or its target no longer exists — one answer for all four, with no owner or lifecycle detail. Also the answer for an image id that is not this share's own.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "A per-caller budget is exhausted: distinct codes tried, or requests of this kind, within a rolling window. `Retry-After` says when the exhausted window admits another request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "description": "The rate-limit protection or the share's authority could not be established, so the request was refused without looking anything up. The link is not invalidated; retry after `Retry-After`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/stack-shares/{code}/images/{imageId}": {
      "get": {
        "operationId": "getStackShareImage",
        "summary": "Stream one image of a stack share",
        "description": "The image bytes themselves, streamed after a fresh check that the link is still active and the image is still the share's own — not a storage URL, and never cacheable (`no-store`, `Accept-Ranges: none`). `If-None-Match` is honoured with a `304` after that same check. This is the only image path: there is no direct storage access behind a share.",
        "tags": [
          "Sharing"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShareCode"
          },
          {
            "$ref": "#/components/parameters/ShareImageId"
          },
          {
            "$ref": "#/components/parameters/ShareImageVariant"
          },
          {
            "$ref": "#/components/parameters/ShareDownload"
          }
        ],
        "responses": {
          "200": {
            "description": "The image bytes, with `Content-Type` naming the stored format.",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "contentEncoding": "binary"
                }
              }
            }
          },
          "304": {
            "description": "The recipient's cached copy is still current. No body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "The link is unknown, expired, revoked or its target no longer exists — one answer for all four, with no owner or lifecycle detail. Also the answer for an image id that is not this share's own.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "A per-caller budget is exhausted: distinct codes tried, or requests of this kind, within a rolling window. `Retry-After` says when the exhausted window admits another request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "description": "The rate-limit protection or the share's authority could not be established, so the request was refused without looking anything up. The link is not invalidated; retry after `Retry-After`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "An RFC 9457 problem detail. Extension members beyond those listed may appear (for example `parameter` on a parameter failure); a client should ignore members it does not know.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "A dereferenceable URL identifying the error code, e.g. `https://www.stackerscan.com/errors/invalid-parameter`.",
            "enum": [
              "https://www.stackerscan.com/errors/invalid-parameter",
              "https://www.stackerscan.com/errors/missing-parameter",
              "https://www.stackerscan.com/errors/unauthorized",
              "https://www.stackerscan.com/errors/not-found",
              "https://www.stackerscan.com/errors/rate-limited",
              "https://www.stackerscan.com/errors/upstream-unavailable",
              "https://www.stackerscan.com/errors/internal-error"
            ]
          },
          "title": {
            "type": "string",
            "description": "A short, occurrence-independent summary of the error code."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status code, repeated in the body."
          },
          "detail": {
            "type": "string",
            "description": "What went wrong on this occurrence — names the offending parameter and the accepted values where applicable. English-only."
          },
          "hint": {
            "type": "string",
            "description": "An optional suggested next action. English-only."
          }
        },
        "additionalProperties": true
      }
    },
    "parameters": {
      "BaseCurrency": {
        "name": "base",
        "in": "query",
        "description": "Currency the prices are converted to, as an ISO 4217 code, `BTC`, or a metal code. Validated against the live catalog from `/api/currencies`, and unenforced in the window where that catalog cannot be read.",
        "required": false,
        "schema": {
          "type": "string",
          "default": "USD",
          "examples": [
            "USD",
            "EUR",
            "BTC"
          ]
        }
      },
      "Currency": {
        "name": "currency",
        "in": "query",
        "description": "Currency the prices are converted to. Same catalog-backed validation as `base`; the two names are the historical spelling difference between endpoints, not a difference in meaning.",
        "required": false,
        "schema": {
          "type": "string",
          "default": "USD",
          "examples": [
            "USD",
            "EUR"
          ]
        }
      },
      "Unit": {
        "name": "unit",
        "in": "query",
        "description": "Weight unit prices are quoted in. Exactly these two spellings — `oz`, `ounce`, `g` and `grams` are rejected (#1122).",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "troy_oz",
            "gram"
          ],
          "default": "troy_oz"
        }
      },
      "Days": {
        "name": "days",
        "in": "query",
        "description": "Size of the trailing window, in days. Strictly an integer: a value such as `90abc` is rejected rather than truncated.",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 3650,
          "default": 90
        }
      },
      "ChartFormat": {
        "name": "format",
        "in": "query",
        "description": "`line` returns closing prices; `ohlc` returns open/high/low/close rows.",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "line",
            "ohlc"
          ],
          "default": "line"
        }
      },
      "ShareCode": {
        "name": "code",
        "in": "path",
        "description": "The five-character short code from a `/s/{code}` link. Letters are case-folded and surrounding whitespace is ignored; anything else — internal spaces, hyphens, the excluded characters `0`, `1`, `i`, `l`, `o` — is not corrected, and answers the same generic `404` as an unknown link. A code is a public, unlisted address, not a secret: the per-caller limits are what constrain guessing.",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^[23456789abcdefghjkmnpqrstuvwxyz]{5}$",
          "minLength": 5,
          "maxLength": 5
        }
      },
      "ShareImageId": {
        "name": "imageId",
        "in": "path",
        "description": "The `id` of one image as listed by the share itself. An individual share admits only its selected image; a gallery admits only its current members. Anything else is the generic `404`, so an id from one share cannot be tried against another.",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "ShareCursor": {
        "name": "cursor",
        "in": "query",
        "description": "The `nextCursor` from the previous page of the same share. Opaque, bound to that share, and unusable as a token of access on its own. Omit it for the first page.",
        "required": false,
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._~:-]+$",
          "maxLength": 16384
        }
      },
      "SharePageSize": {
        "name": "pageSize",
        "in": "query",
        "description": "Images per gallery page. Individual shares ignore it.",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 50,
          "default": 24
        }
      },
      "ShareImageVariant": {
        "name": "variant",
        "in": "query",
        "description": "`full` streams the stored image; `thumbnail` streams its reduced rendition.",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "full",
            "thumbnail"
          ],
          "default": "full"
        }
      },
      "ShareDownload": {
        "name": "download",
        "in": "query",
        "description": "`true` answers the same bytes with an attachment `Content-Disposition` under a neutral file name — the protected Save image path. Nothing about the owner's original file name is disclosed.",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "true",
            "false"
          ],
          "default": "false"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "A parameter was missing or outside its accepted values. `type` is `missing-parameter` or `invalid-parameter`, and `detail` names the parameter.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ServerError": {
        "description": "The request was well formed but could not be served. Retrying later may succeed.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "PlatformRejected": {
        "description": "Refused before the handler ran — the hosting platform's abuse protection (for example a rate limit or challenge). Unlike application failures, the body is not guaranteed to be `application/problem+json`."
      }
    }
  }
}