{
  "openapi": "3.1.0",
  "info": {
    "title": "Denpasoft Public API",
    "version": "1.0.0",
    "summary": "Read-only public catalog API for the Denpasoft storefront.",
    "description": "Free, identity-free, read-only endpoints serving the published Denpasoft catalog from the edge. No API keys or authentication. Documented endpoints follow an additive-only stability policy: new optional fields may appear in responses; existing fields will not change meaning; breaking changes get a new path. Human-readable documentation lives at /developers.\n\nData licensing: parts of the catalog's tag/metadata vocabulary contain information from VNDB (https://vndb.org), made available under the Open Database License (ODbL) v1.0 with contents under the Database Contents License (DbCL) v1.0. JSON envelopes carry a machine-readable `license` stanza; full terms, scope, and attribution requirements for redistributors live at /policy/data-licenses/.",
    "termsOfService": "https://api.denpasoft.com/developers",
    "contact": {
      "name": "Denpasoft developer support",
      "url": "https://api.denpasoft.com/developers"
    }
  },
  "externalDocs": {
    "description": "Developer guide: conventions, the SearchHit shape, feeds, fair use, and licensing.",
    "url": "https://api.denpasoft.com/developers"
  },
  "servers": [
    {
      "url": "https://api.denpasoft.com"
    },
    {
      "url": "https://beta.denpasoft.com"
    }
  ],
  "tags": [
    {
      "name": "Search",
      "description": "Full-text search over the published catalog."
    },
    {
      "name": "Catalog",
      "description": "Browse and hydrate published catalog products."
    },
    {
      "name": "Recommendations",
      "description": "Similar-title suggestions from a seed set."
    },
    {
      "name": "Embedding",
      "description": "oEmbed provider for embeddable storefront pages."
    }
  ],
  "paths": {
    "/api/search": {
      "get": {
        "operationId": "search",
        "tags": [
          "Search"
        ],
        "summary": "Instant text search over the published catalog",
        "description": "Returns product hits matching the query text. Unknown query parameters are rejected with 400; responses are no-store.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search text. Fewer than 2 characters yields an empty result list (still 200).",
            "schema": {
              "type": "string",
              "maxLength": 1024
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of hits, 1-12. Defaults to 8. Out-of-range or non-numeric values are clamped or fall back to the default rather than erroring.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12,
              "default": 8
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching product hits. An empty results array means no matches.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultsEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Unknown query parameter (the parameter set is closed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (per-IP). Back off and retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/catalog/by-ids": {
      "get": {
        "operationId": "catalogByIds",
        "tags": [
          "Catalog"
        ],
        "summary": "Hydrate product ids into product hit objects",
        "description": "Resolves a comma-separated list of product database ids into product cards, preserving input order. Unknown query parameters are rejected with 400; responses are no-store.",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "required": true,
            "description": "Comma-separated list of positive integer product ids. At most 20 are used.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+(,[0-9]+)*$",
              "examples": [
                "101,102,103"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved product hits in input order. Unknown ids are omitted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultsEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Missing/empty/malformed ids parameter, or an unknown query parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (per-IP). Back off and retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/recommend": {
      "get": {
        "operationId": "recommend",
        "tags": [
          "Recommendations"
        ],
        "summary": "Similar titles by example",
        "description": "Returns up to 8 catalog picks similar to a seed set of product ids (the seeds are excluded). Fail-open: an empty results array means no suggestions, not an error. Unknown query parameters are rejected with 400; responses are no-store.",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "required": true,
            "description": "Comma-separated list of positive integer seed product ids. At most 10 are used.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+(,[0-9]+)*$",
              "examples": [
                "101,102"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Up to 8 similar-title picks. An empty results array means no suggestions are available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultsEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Missing/empty/malformed ids parameter, or an unknown query parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (per-IP). Back off and retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/oembed": {
      "get": {
        "operationId": "oembed",
        "tags": [
          "Embedding"
        ],
        "summary": "oEmbed 1.0 provider (link type) for embeddable storefront pages",
        "description": "oEmbed 1.0 provider (link type) for the storefront's embeddable pages — public user profiles and product pages. Non-embeddable or unknown URLs return 404; responses are no-store.",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "Absolute URL of the page to embed. Must be on the same host as the request.",
            "schema": {
              "type": "string",
              "format": "uri"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Response format. Only json is supported; any other value returns 501.",
            "schema": {
              "type": "string",
              "enum": [
                "json"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "oEmbed 1.0 link payload for the embeddable page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OEmbedLink"
                }
              }
            }
          },
          "400": {
            "description": "Missing or unparseable url parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "The url is not an embeddable resource (wrong host, unknown page, or private).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (per-IP). Back off and retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "501": {
            "description": "Unsupported format (only json is implemented).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/catalog/list": {
      "get": {
        "operationId": "listPublicCatalogProducts",
        "tags": [
          "Catalog"
        ],
        "summary": "List published catalog products, paginated",
        "description": "Returns one page of the published catalog as product cards, with total counts for pagination. Unknown query parameters are rejected with 400; responses are no-store.",
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "Listing scope. Unsupported values or a missing required taxonomy slug return an empty page.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "new",
                "sale",
                "featured",
                "category",
                "brand",
                "developer"
              ]
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Taxonomy slug; required when scope=category.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "publisher",
            "in": "query",
            "required": false,
            "description": "Brand/publisher slug; required when scope=brand, or a cross-axis filter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "developer",
            "in": "query",
            "required": false,
            "description": "Developer slug; required when scope=developer, or a cross-axis filter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number. Out-of-range or non-numeric values fall back to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Page size, clamped to 1..48.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 48,
              "default": 24
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A public catalog page. The empty shape is the fail-open response for unbound, incomplete, or unserviceable mirror reads.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogListResponse"
                }
              }
            }
          },
          "400": {
            "description": "Unknown query parameter (the parameter set is closed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (per-IP). Back off and retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SearchHit": {
        "type": "object",
        "properties": {
          "databaseId": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Stable product id."
          },
          "slug": {
            "type": "string",
            "description": "URL slug; the product page is /product/{slug}."
          },
          "name": {
            "type": "string",
            "description": "Product title."
          },
          "imageUrl": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Cover image URL, or null when none is available."
          },
          "regularPrice": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Regular price as a decimal string, or null."
          },
          "salePrice": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Sale price as a decimal string, or null when not on sale."
          },
          "onSale": {
            "type": "boolean",
            "description": "Whether the product is currently on sale."
          }
        },
        "required": [
          "databaseId",
          "slug",
          "name",
          "imageUrl",
          "regularPrice",
          "salePrice",
          "onSale"
        ],
        "additionalProperties": true,
        "description": "A product card object. New optional fields may be added over time (additive-only stability policy); consumers should ignore unrecognized fields."
      },
      "ResultsEnvelope": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchHit"
            }
          },
          "license": {
            "$ref": "#/components/schemas/DataLicense"
          }
        },
        "required": [
          "results",
          "license"
        ],
        "additionalProperties": true,
        "description": "Standard envelope for product-list responses."
      },
      "CatalogListResponse": {
        "type": "object",
        "properties": {
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchHit"
            }
          },
          "total": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Total products matching the scope across all pages."
          },
          "totalPages": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Total number of pages at the requested per_page size."
          }
        },
        "required": [
          "products",
          "total",
          "totalPages"
        ],
        "additionalProperties": true,
        "description": "A single page of the public catalog projection. The empty shape ({ products: [], total: 0, totalPages: 0 }) is the fail-open response for an unbound, incomplete, or unserviceable mirror read."
      },
      "DataLicense": {
        "type": "object",
        "properties": {
          "notice": {
            "type": "string",
            "const": "Contains information from VNDB (https://vndb.org), which is made available under the Open Database License (ODbL) v1.0.",
            "description": "Human-readable ODbL attribution notice."
          },
          "source": {
            "type": "string",
            "const": "https://vndb.org/"
          },
          "spdx": {
            "type": "string",
            "const": "ODbL-1.0",
            "description": "SPDX identifier of the database license."
          },
          "licenseUrl": {
            "type": "string",
            "const": "https://opendatacommons.org/licenses/odbl/1-0/"
          },
          "detailsUrl": {
            "type": "string",
            "const": "https://api.denpasoft.com/policy/data-licenses/",
            "description": "Human-readable terms, scope, and share-alike availability statement."
          }
        },
        "required": [
          "notice",
          "source",
          "spdx",
          "licenseUrl",
          "detailsUrl"
        ],
        "additionalProperties": true,
        "description": "Data-licensing stanza. Catalog tag/metadata fields derive in part from the VNDB database (ODbL 1.0 + DbCL 1.0); this object carries the required attribution notice. Redistributors of the tag data must preserve the attribution — see detailsUrl for scope and full terms."
      },
      "ApiError": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation."
          }
        },
        "required": [
          "code",
          "message"
        ],
        "additionalProperties": true,
        "description": "Standard JSON error body."
      },
      "OEmbedLink": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string",
            "const": "1.0"
          },
          "type": {
            "type": "string",
            "const": "link"
          },
          "title": {
            "type": "string",
            "description": "Title of the embedded resource."
          },
          "author_name": {
            "description": "Present when the resource has an author.",
            "type": "string"
          },
          "author_url": {
            "description": "Present when the resource has an author page.",
            "type": "string"
          },
          "provider_name": {
            "type": "string",
            "const": "Denpasoft"
          },
          "provider_url": {
            "type": "string",
            "description": "Origin of the serving host."
          },
          "thumbnail_url": {
            "description": "Present when the resource has a shareable thumbnail.",
            "type": "string"
          }
        },
        "required": [
          "version",
          "type",
          "title",
          "provider_name",
          "provider_url"
        ],
        "additionalProperties": true,
        "description": "oEmbed 1.0 response of type link."
      }
    }
  }
}
