{
  "openapi": "3.1.0",
  "x-build-version": "0.0.1+202608262147",
  "x-base-url": "https://builderbooks.dev",
  "info": {
    "title": "BuilderBooks.dev catalogue API",
    "version": "1.0.0",
    "summary": "Read the catalogue as JSON instead of scraping HTML.",
    "description": "Every public page has a JSON twin and a Markdown twin. The JSON twins are read-only, need no key and answer with CORS open to any origin. Prices are net, VAT is added at checkout. Every JSON response, success and error alike, carries the standard headers x-build-version and x-base-url (both exposed via access-control-expose-headers), identifying the build that answered and the canonical origin it speaks for. They are declared per response under components/responses and never omitted.",
    "license": {
      "name": "All rights reserved",
      "url": "https://builderbooks.dev/terms"
    },
    "contact": {
      "name": "BuilderBooks.dev",
      "url": "https://builderbooks.dev/contact",
      "email": "contact@pa.ventures"
    },
    "x-deprecation-policy": "Breaking changes ship under a new major prefix (/v2/). The previous major stays available for at least six months after its successor is announced, and responses on a retiring version carry deprecation and sunset headers first.",
    "x-rate-limit": "600 requests per hour per client, declared on every response via ratelimit-policy, ratelimit-limit, ratelimit-remaining and ratelimit-reset.",
    "x-standard-headers": [
      {
        "name": "x-build-version",
        "header": {
          "$ref": "#/components/headers/BuildVersion"
        },
        "on": "every JSON response, including errors and 304 revalidations"
      },
      {
        "name": "x-base-url",
        "header": {
          "$ref": "#/components/headers/BaseUrl"
        },
        "on": "every JSON response, including errors and 304 revalidations"
      }
    ]
  },
  "externalDocs": {
    "description": "Developer guide",
    "url": "https://builderbooks.dev/developers"
  },
  "servers": [
    {
      "url": "https://builderbooks.dev",
      "description": "Production"
    },
    {
      "url": "https://builderbooks.dev/v1",
      "description": "Production, pinned to major version 1"
    }
  ],
  "tags": [
    {
      "name": "catalogue",
      "description": "Skills and bundles."
    },
    {
      "name": "health",
      "description": "Operational status."
    }
  ],
  "paths": {
    "/version.json": {
      "get": {
        "operationId": "getVersion",
        "summary": "Lightweight compatibility check.",
        "description": "Returns the build version and canonical base URL as JSON, with the same values repeated as response headers. Useful for fast agent health checks.",
        "tags": [
          "catalogue"
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/VersionOk"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApi",
        "summary": "This document.",
        "tags": [
          "catalogue"
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OpenApiOk"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/catalog.json": {
      "get": {
        "operationId": "getCatalog",
        "summary": "The whole catalogue in one read.",
        "description": "Every published skill and every bundle with at least two live skills. Narrow it with cluster, sort and limit so you fetch only what you need. A bundle drops out when fewer than two of its skills match the filter, but a listed bundle always shows its full skill list, because a bundle is sold whole.",
        "tags": [
          "catalogue"
        ],
        "parameters": [
          {
            "name": "cluster",
            "in": "query",
            "required": false,
            "description": "Keep only skills in these job clusters. Repeat the parameter or pass a comma separated list. A skill matches on either of its two clusters.",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "ideas",
                  "ship",
                  "design",
                  "activation",
                  "positioning",
                  "pricing",
                  "gtm"
                ]
              }
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Order of the skills. Default is catalogue order. Price sorts fall back to title on a tie.",
            "schema": {
              "type": "string",
              "enum": [
                "default",
                "price_asc",
                "price_desc",
                "title"
              ],
              "default": "default"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Keep only the first N skills after sorting. Bundles are not limited.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/CatalogOk"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/skills/{slug}.json": {
      "get": {
        "operationId": "getSkill",
        "summary": "One skill.",
        "tags": [
          "catalogue"
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The slug as it appears in the page URL.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/SkillOk"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/bundles/{slug}.json": {
      "get": {
        "operationId": "getBundle",
        "summary": "One bundle.",
        "tags": [
          "catalogue"
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The slug as it appears in the page URL.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/BundleOk"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/public/health/stripe": {
      "get": {
        "operationId": "getStripeHealth",
        "summary": "Status of the payment connection.",
        "description": "Carries no secrets: status, key mode, API host, round trip time and issue codes.",
        "tags": [
          "health"
        ],
        "parameters": [
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "Set to 1 to re-run the check instead of serving the cached result.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/StripeHealthOk"
          },
          "503": {
            "description": "Unreachable or misconfigured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StripeHealth"
                }
              }
            },
            "headers": {
              "x-build-version": {
                "$ref": "#/components/headers/BuildVersion"
              },
              "x-base-url": {
                "$ref": "#/components/headers/BaseUrl"
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "VersionOk": {
        "description": "The build identity of this deployment.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Version"
            }
          }
        },
        "headers": {
          "x-build-version": {
            "$ref": "#/components/headers/BuildVersion"
          },
          "x-base-url": {
            "$ref": "#/components/headers/BaseUrl"
          }
        }
      },
      "OpenApiOk": {
        "description": "The OpenAPI description of the JSON twins.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object"
            }
          }
        },
        "headers": {
          "x-build-version": {
            "$ref": "#/components/headers/BuildVersion"
          },
          "x-base-url": {
            "$ref": "#/components/headers/BaseUrl"
          }
        }
      },
      "CatalogOk": {
        "description": "The catalogue.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Catalog"
            }
          }
        },
        "headers": {
          "x-build-version": {
            "$ref": "#/components/headers/BuildVersion"
          },
          "x-base-url": {
            "$ref": "#/components/headers/BaseUrl"
          }
        }
      },
      "SkillOk": {
        "description": "The skill.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Skill"
            }
          }
        },
        "headers": {
          "x-build-version": {
            "$ref": "#/components/headers/BuildVersion"
          },
          "x-base-url": {
            "$ref": "#/components/headers/BaseUrl"
          }
        }
      },
      "BundleOk": {
        "description": "The bundle.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Bundle"
            }
          }
        },
        "headers": {
          "x-build-version": {
            "$ref": "#/components/headers/BuildVersion"
          },
          "x-base-url": {
            "$ref": "#/components/headers/BaseUrl"
          }
        }
      },
      "StripeHealthOk": {
        "description": "Reachable, healthy or degraded.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/StripeHealth"
            }
          }
        },
        "headers": {
          "x-build-version": {
            "$ref": "#/components/headers/BuildVersion"
          },
          "x-base-url": {
            "$ref": "#/components/headers/BaseUrl"
          }
        }
      },
      "NotFound": {
        "description": "No published document at this path.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "x-build-version": {
            "$ref": "#/components/headers/BuildVersion"
          },
          "x-base-url": {
            "$ref": "#/components/headers/BaseUrl"
          }
        }
      },
      "BadRequest": {
        "description": "An unknown parameter value. Nothing is guessed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "x-build-version": {
            "$ref": "#/components/headers/BuildVersion"
          },
          "x-base-url": {
            "$ref": "#/components/headers/BaseUrl"
          }
        }
      },
      "ServerError": {
        "description": "The catalogue could not be read. The build stamp still identifies the deploy.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "x-build-version": {
            "$ref": "#/components/headers/BuildVersion"
          },
          "x-base-url": {
            "$ref": "#/components/headers/BaseUrl"
          }
        }
      }
    },
    "headers": {
      "BuildVersion": {
        "description": "The build that answered. Standard on every JSON response, success and error alike. Compare it across calls to detect a deploy mid-session.",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "BaseUrl": {
        "description": "The canonical origin this build serves, without a trailing slash. Standard on every JSON response, success and error alike.",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uri"
        }
      }
    },
    "schemas": {
      "Version": {
        "type": "object",
        "required": [
          "x-build-version",
          "x-base-url"
        ],
        "properties": {
          "x-build-version": {
            "type": "string",
            "description": "The build that answered. Compare it across calls to detect a deploy mid-session."
          },
          "x-base-url": {
            "type": "string",
            "format": "uri",
            "description": "The canonical origin this build serves, without a trailing slash."
          }
        }
      },
      "Cluster": {
        "type": "object",
        "required": [
          "id",
          "label",
          "question"
        ],
        "properties": {
          "id": {
            "type": "string",
            "enum": [
              "ideas",
              "ship",
              "design",
              "activation",
              "positioning",
              "pricing",
              "gtm"
            ]
          },
          "label": {
            "type": "string",
            "examples": [
              "Validation"
            ]
          },
          "question": {
            "type": "string",
            "examples": [
              "Does anyone want it?"
            ]
          }
        },
        "description": "A job cluster: the question the book answers."
      },
      "Price": {
        "type": "object",
        "required": [
          "amount_cents",
          "currency",
          "tax"
        ],
        "properties": {
          "amount_cents": {
            "type": "integer",
            "description": "Net price in euro cents."
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ]
          },
          "tax": {
            "type": "string",
            "enum": [
              "net, VAT added at checkout"
            ]
          }
        }
      },
      "Skill": {
        "type": "object",
        "required": [
          "type",
          "slug",
          "url",
          "markdown_url",
          "json_url",
          "title",
          "author",
          "clusters",
          "commands",
          "command_count",
          "kind",
          "you_get",
          "wont_do",
          "price",
          "install_url",
          "cart_url"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "skill"
            ]
          },
          "slug": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "markdown_url": {
            "type": "string",
            "format": "uri"
          },
          "json_url": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "author": {
            "type": "string"
          },
          "problem": {
            "type": [
              "string",
              "null"
            ]
          },
          "outcome": {
            "type": [
              "string",
              "null"
            ]
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "clusters": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "label",
                "question"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "enum": [
                    "ideas",
                    "ship",
                    "design",
                    "activation",
                    "positioning",
                    "pricing",
                    "gtm"
                  ]
                },
                "label": {
                  "type": "string",
                  "examples": [
                    "Validation"
                  ]
                },
                "question": {
                  "type": "string",
                  "examples": [
                    "Does anyone want it?"
                  ]
                }
              },
              "description": "A job cluster: the question the book answers."
            },
            "maxItems": 2
          },
          "commands": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "command_count": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "command",
              "reference"
            ],
            "description": "A command skill exposes slash commands, a reference skill is read by the agent."
          },
          "you_get": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "wont_do": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "source": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "year",
              "edition",
              "isbn",
              "buy_url",
              "buy_label"
            ],
            "properties": {
              "year": {
                "type": "integer"
              },
              "edition": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isbn": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "buy_url": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri"
              },
              "buy_label": {
                "type": "string"
              }
            }
          },
          "price": {
            "type": "object",
            "required": [
              "amount_cents",
              "currency",
              "tax"
            ],
            "properties": {
              "amount_cents": {
                "type": "integer",
                "description": "Net price in euro cents."
              },
              "currency": {
                "type": "string",
                "enum": [
                  "EUR"
                ]
              },
              "tax": {
                "type": "string",
                "enum": [
                  "net, VAT added at checkout"
                ]
              }
            }
          },
          "install_url": {
            "type": "string",
            "format": "uri"
          },
          "cart_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Bundle": {
        "type": "object",
        "required": [
          "type",
          "slug",
          "url",
          "markdown_url",
          "json_url",
          "title",
          "problem",
          "blurb",
          "why",
          "skills",
          "price",
          "cart_url"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "bundle"
            ]
          },
          "slug": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "markdown_url": {
            "type": "string",
            "format": "uri"
          },
          "json_url": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "problem": {
            "type": "string"
          },
          "blurb": {
            "type": "string"
          },
          "why": {
            "type": "string"
          },
          "skills": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "slug",
                "title",
                "author",
                "url",
                "json_url"
              ],
              "properties": {
                "slug": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "author": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "json_url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "price": {
            "type": "object",
            "required": [
              "skill_count",
              "list_cents",
              "amount_cents",
              "currency",
              "tax"
            ],
            "properties": {
              "skill_count": {
                "type": "integer"
              },
              "list_cents": {
                "type": "integer",
                "description": "Sum of the single prices, before the tier discount."
              },
              "amount_cents": {
                "type": "integer",
                "description": "What the bundle costs. Every third skill is on us."
              },
              "currency": {
                "type": "string",
                "enum": [
                  "EUR"
                ]
              },
              "tax": {
                "type": "string",
                "enum": [
                  "net, VAT added at checkout"
                ]
              }
            }
          },
          "cart_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Catalog": {
        "type": "object",
        "required": [
          "version",
          "site",
          "generated_at",
          "query",
          "pricing",
          "skill_count",
          "skills",
          "bundles"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "description": "Bumped when a field is removed or changes meaning."
          },
          "site": {
            "type": "string",
            "format": "uri"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "query": {
            "type": "object",
            "description": "The filter and sort that produced this document, echoed back.",
            "required": [
              "clusters",
              "sort",
              "limit"
            ],
            "properties": {
              "clusters": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "ideas",
                    "ship",
                    "design",
                    "activation",
                    "positioning",
                    "pricing",
                    "gtm"
                  ]
                }
              },
              "sort": {
                "type": "string",
                "enum": [
                  "default",
                  "price_asc",
                  "price_desc",
                  "title"
                ]
              },
              "limit": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "pricing": {
            "type": "object",
            "required": [
              "single_cents",
              "currency",
              "rule"
            ],
            "properties": {
              "single_cents": {
                "type": "integer"
              },
              "currency": {
                "type": "string",
                "enum": [
                  "EUR"
                ]
              },
              "rule": {
                "type": "string"
              }
            }
          },
          "skill_count": {
            "type": "integer",
            "description": "How many skills matched the filter, before limit cut the list."
          },
          "skills": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Skill"
            }
          },
          "bundles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Bundle"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code",
          "message",
          "hints",
          "documentation_url"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "not_found",
              "bad_request",
              "server_error"
            ],
            "description": "Kept as the original field name. Always equal to `code`."
          },
          "code": {
            "type": "string",
            "enum": [
              "not_found",
              "bad_request",
              "server_error"
            ],
            "description": "The stable machine-readable reason, safe to branch on."
          },
          "message": {
            "type": "string",
            "description": "One sentence, written for a human reading a log."
          },
          "hints": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "URLs that answer the question the failed call was asking."
          },
          "documentation_url": {
            "type": "string",
            "format": "uri",
            "description": "The developer guide for this API."
          }
        }
      },
      "StripeHealth": {
        "type": "object",
        "required": [
          "service",
          "status",
          "checkedAt",
          "issues"
        ],
        "properties": {
          "service": {
            "type": "string",
            "enum": [
              "stripe"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded",
              "down"
            ]
          },
          "mode": {
            "type": "string"
          },
          "reportedMode": {
            "type": "string"
          },
          "apiBaseHost": {
            "type": "string"
          },
          "apiVersion": {
            "type": "string"
          },
          "managedPayments": {
            "type": "boolean"
          },
          "productsWithoutTaxCode": {
            "type": "integer"
          },
          "reachable": {
            "type": "boolean"
          },
          "latencyMs": {
            "type": "number"
          },
          "checkedAt": {
            "type": "string",
            "format": "date-time"
          },
          "issues": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "level",
                "code"
              ],
              "properties": {
                "level": {
                  "type": "string"
                },
                "code": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  }
}
