{
  "openapi": "3.1.1",
  "info": {
    "title": "Eulernen catalogue API",
    "version": "1.2.1",
    "summary": "Read-only catalogue of Eulernen learning-poster sets for German primary school.",
    "description": "All poster sets, poster titles and shared delivery and licence facts, as one static file (getCatalog) or filtered on the server (listSets, getSet). Public, anonymous and read-only: no registration, API key or sandbox (see https://eulernen.de/auth.md); nothing can be bought or changed through it. Prices and purchase only on Etsy, where the listings are being added.\n\nVersioning: the major version is part of the path (/api/v1/). Changes within v1 stay backwards compatible; new fields may appear. A breaking change gets a new path such as /api/v2/; the old path then stays available and is announced with Deprecation (RFC 9745) and Sunset (RFC 8594) response headers before it is removed. /api/catalog.json is an alias of /api/v1/catalog.json.\n\nErrors: every error under /api/ is RFC 9457 application/problem+json (schema Problem).\n\nRate limit: /api/, /mcp and /a2a share 60 requests per 60 seconds per client IP, counted per Cloudflare location. Every response carries RateLimit-Policy (draft-ietf-httpapi-ratelimit-headers); above the limit the answer is 429 with Retry-After.",
    "contact": {
      "name": "Eulernen",
      "email": "info@eulernen.de",
      "url": "https://eulernen.de/"
    }
  },
  "servers": [
    {
      "url": "https://eulernen.de"
    }
  ],
  "security": [],
  "externalDocs": {
    "description": "Developer docs: catalogue and query API, MCP, A2A, errors, rate limit and versioning",
    "url": "https://eulernen.de/docs"
  },
  "paths": {
    "/api/v1/catalog.json": {
      "get": {
        "operationId": "getCatalog",
        "summary": "Get all poster sets",
        "description": "Returns every Eulernen poster set with subject, grades, edition, style and poster titles, plus the shop link, delivery options and licence. To filter on the server, use listSets.",
        "responses": {
          "200": {
            "description": "The full catalogue.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Catalog"
                }
              }
            }
          },
          "404": {
            "description": "Unknown path under /api/, for example a mistyped file name, an unpublished version or an unknown set id.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "A method other than GET or HEAD; the API is read-only.",
            "headers": {
              "Allow": {
                "description": "Supported methods.",
                "schema": {
                  "type": "string",
                  "const": "GET, HEAD"
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from this client IP to /api/, /mcp and /a2a together.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before the next request.",
                "schema": {
                  "type": "integer",
                  "const": 60
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected error in the worker; the body explains it and when to retry.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sets": {
      "get": {
        "operationId": "listSets",
        "summary": "Find poster sets by subject, grade, edition and style",
        "description": "Returns the sets that match every given filter, without poster titles, plus the shop link and sales status. All filters are optional; without any, all sets are returned. Unknown parameters and values outside the enums are rejected with 400. Use getSet for the poster titles of one set.",
        "parameters": [
          {
            "name": "subject",
            "in": "query",
            "required": false,
            "description": "Subject; HSU = Heimat- und Sachunterricht.",
            "schema": {
              "type": "string",
              "enum": [
                "Mathe",
                "Deutsch",
                "Englisch",
                "HSU"
              ]
            }
          },
          {
            "name": "grade",
            "in": "query",
            "required": false,
            "description": "Grade (Klasse) the set must cover.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 4
            }
          },
          {
            "name": "edition",
            "in": "query",
            "required": false,
            "description": "Bayern = aligned with LehrplanPLUS Bayern; Allgemein = general edition (edition null in the response).",
            "schema": {
              "type": "string",
              "enum": [
                "Bayern",
                "Allgemein"
              ]
            }
          },
          {
            "name": "style",
            "in": "query",
            "required": false,
            "description": "Visual style.",
            "schema": {
              "type": "string",
              "enum": [
                "Klassisch",
                "Aquarell"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The matching sets; an empty list when nothing matches.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetList"
                }
              }
            }
          },
          "400": {
            "description": "An unknown query parameter or a value outside its enum.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "A method other than GET or HEAD; the API is read-only.",
            "headers": {
              "Allow": {
                "description": "Supported methods.",
                "schema": {
                  "type": "string",
                  "const": "GET, HEAD"
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from this client IP to /api/, /mcp and /a2a together.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before the next request.",
                "schema": {
                  "type": "integer",
                  "const": 60
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected error in the worker; the body explains it and when to retry.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sets/{id}": {
      "get": {
        "operationId": "getSet",
        "summary": "Get one poster set with its poster titles",
        "description": "Returns one set with all poster titles, plus the shop link and sales status. An unknown id is a 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Set id from listSets or getCatalog.",
            "schema": {
              "type": "string",
              "examples": [
                "mathe12_bayern_aquarell"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The set.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown path under /api/, for example a mistyped file name, an unpublished version or an unknown set id.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "A method other than GET or HEAD; the API is read-only.",
            "headers": {
              "Allow": {
                "description": "Supported methods.",
                "schema": {
                  "type": "string",
                  "const": "GET, HEAD"
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from this client IP to /api/, /mcp and /a2a together.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before the next request.",
                "schema": {
                  "type": "integer",
                  "const": 60
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected error in the worker; the body explains it and when to retry.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RateLimitPolicy": {
        "description": "Quota policy as an IETF RateLimit-Policy structured field (draft-ietf-httpapi-ratelimit-headers): q requests per w seconds per client IP, shared by /api/, /mcp and /a2a.",
        "schema": {
          "type": "string",
          "const": "\"default\";q=60;w=60"
        }
      }
    },
    "schemas": {
      "Catalog": {
        "type": "object",
        "description": "The whole Eulernen catalogue.",
        "required": [
          "name",
          "shop",
          "availability",
          "contact",
          "formats",
          "delivery",
          "licence",
          "sets"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Brand name."
          },
          "description": {
            "type": "string",
            "description": "What Eulernen offers (German)."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Website."
          },
          "shop": {
            "type": "string",
            "format": "uri",
            "description": "The only shop (Etsy)."
          },
          "availability": {
            "type": "string",
            "description": "Sales status; no prices or stock are published here."
          },
          "contact": {
            "type": "string",
            "format": "email",
            "description": "Contact e-mail address."
          },
          "formats": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Paper sizes, e.g. DIN A4."
          },
          "delivery": {
            "type": "object",
            "description": "print: one single poster per order line; pdf: every poster of a set as a digital download.",
            "properties": {
              "print": {
                "$ref": "#/components/schemas/Delivery"
              },
              "pdf": {
                "$ref": "#/components/schemas/Delivery"
              }
            }
          },
          "licence": {
            "type": "string",
            "description": "Usage rights for the PDF files."
          },
          "editions": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Edition name to description, e.g. Bayern."
          },
          "styles": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Style name to description: Klassisch and Aquarell."
          },
          "sets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PosterSet"
            },
            "description": "All poster sets."
          }
        }
      },
      "Delivery": {
        "type": "object",
        "description": "One way to receive posters.",
        "properties": {
          "content": {
            "type": "string",
            "description": "What one purchase contains."
          },
          "formats": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Available paper sizes."
          },
          "shipping": {
            "type": "string",
            "description": "Printing and shipping, or download."
          },
          "frame": {
            "type": "string",
            "description": "Whether a frame is included (print only)."
          }
        }
      },
      "SetSummary": {
        "type": "object",
        "description": "One set of learning posters for a subject and grade band, without poster titles.",
        "required": [
          "id",
          "name",
          "subject",
          "grades",
          "edition",
          "style",
          "posterCount"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable set id.",
            "examples": [
              "mathe12_bayern_aquarell"
            ]
          },
          "name": {
            "type": "string",
            "description": "Set name (German)."
          },
          "subject": {
            "type": "string",
            "enum": [
              "Mathe",
              "Deutsch",
              "Englisch",
              "HSU"
            ],
            "description": "HSU = Heimat- und Sachunterricht."
          },
          "grades": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 1,
              "maximum": 4
            },
            "description": "Grades (Klassen) the set is for."
          },
          "edition": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "Bayern",
              null
            ],
            "description": "Bayern = aligned with LehrplanPLUS Bayern; null = general edition."
          },
          "style": {
            "type": "string",
            "enum": [
              "Klassisch",
              "Aquarell"
            ],
            "description": "Visual style."
          },
          "posterCount": {
            "type": "integer",
            "minimum": 1,
            "description": "Number of posters in the set."
          }
        }
      },
      "PosterSet": {
        "type": "object",
        "description": "One set of learning posters for a subject and grade band, with poster titles.",
        "allOf": [
          {
            "$ref": "#/components/schemas/SetSummary"
          }
        ],
        "required": [
          "posters"
        ],
        "properties": {
          "posters": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Poster titles (German)."
          }
        }
      },
      "SetList": {
        "type": "object",
        "description": "Result of listSets.",
        "required": [
          "sets",
          "shop",
          "availability"
        ],
        "properties": {
          "sets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SetSummary"
            },
            "description": "Matching sets in catalogue order."
          },
          "shop": {
            "type": "string",
            "format": "uri",
            "description": "The only shop (Etsy)."
          },
          "availability": {
            "type": "string",
            "description": "Sales status; no prices or stock are published here."
          }
        }
      },
      "SetResponse": {
        "type": "object",
        "description": "Result of getSet.",
        "required": [
          "set",
          "shop",
          "availability"
        ],
        "properties": {
          "set": {
            "$ref": "#/components/schemas/PosterSet",
            "description": "The requested set."
          },
          "shop": {
            "type": "string",
            "format": "uri",
            "description": "The only shop (Etsy)."
          },
          "availability": {
            "type": "string",
            "description": "Sales status; no prices or stock are published here."
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details, sent as application/problem+json.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "instance",
          "resolution"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri-reference",
            "description": "Problem type; about:blank means the HTTP status code is the error code.",
            "examples": [
              "about:blank"
            ]
          },
          "title": {
            "type": "string",
            "description": "HTTP reason phrase.",
            "examples": [
              "Not Found"
            ]
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code.",
            "examples": [
              404
            ]
          },
          "detail": {
            "type": "string",
            "description": "What went wrong with this request."
          },
          "instance": {
            "type": "string",
            "format": "uri-reference",
            "description": "The requested path."
          },
          "resolution": {
            "type": "string",
            "description": "How to recover, e.g. the right URL or method."
          }
        }
      }
    }
  }
}
