{
  "openapi": "3.1.0",
  "info": {
    "title": "Toilet Paper Math API",
    "version": "2026-10-24",
    "description": "Read-only data about Charmin, Quilted Northern, Angel Soft or Kirkland Signature toilet paper rolls, and a price comparison. Free to use; please link to https://toiletpaper.app."
  },
  "servers": [
    {
      "url": "https://toiletpaper.app"
    }
  ],
  "externalDocs": {
    "description": "Data & API",
    "url": "https://toiletpaper.app/data"
  },
  "paths": {
    "/api/products": {
      "get": {
        "operationId": "listProducts",
        "summary": "Every brand and roll",
        "responses": {
          "200": {
            "description": "Brands and rolls. Cached for an hour, with an ETag.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "checked": {
                      "type": "string"
                    },
                    "honestRollSqFt": {
                      "type": "number"
                    },
                    "softness": {
                      "type": "object",
                      "properties": {
                        "checked": {
                          "type": "string"
                        },
                        "outOf": {
                          "type": "integer"
                        },
                        "method": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "about": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    },
                    "brands": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "maker": {
                            "type": "string"
                          },
                          "regularRollId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "regularRollSheets": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "products": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Product"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/compare": {
      "get": {
        "operationId": "comparePacks",
        "summary": "Compare 2 to 8 packs by price, softness or a balance of both",
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "description": "sheets: per sheet (default). area: per square foot. areaply: per square foot × ply.",
            "schema": {
              "type": "string",
              "enum": [
                "sheets",
                "area",
                "areaply"
              ],
              "default": "sheets"
            }
          },
          {
            "name": "p",
            "in": "query",
            "required": false,
            "description": "What matters more. cheap: lowest price per unit (default). balanced: a softer pack wins if it costs at most 10% more per unit for each softness point it gains over the cheapest pack. soft: the softest rated pack wins, price breaks ties. Packs that aren't rated can't win on softness.",
            "schema": {
              "type": "string",
              "enum": [
                "cheap",
                "balanced",
                "soft"
              ],
              "default": "cheap"
            }
          },
          {
            "name": "a",
            "in": "query",
            "required": true,
            "description": "Pack A: <product id>,<rolls>,<price>. A custom roll is custom:<sheets>:<width>x<length>:<ply>.",
            "schema": {
              "type": "string"
            },
            "example": "charmin-ultra-soft-mega-xl,6,9.99"
          },
          {
            "name": "b",
            "in": "query",
            "required": true,
            "description": "Pack B: <product id>,<rolls>,<price>. A custom roll is custom:<sheets>:<width>x<length>:<ply>.",
            "schema": {
              "type": "string"
            },
            "example": "quilted-northern-ultra-soft-strong-mega,12,13.49"
          },
          {
            "name": "c",
            "in": "query",
            "required": false,
            "description": "Pack C: <product id>,<rolls>,<price>. A custom roll is custom:<sheets>:<width>x<length>:<ply>.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "d",
            "in": "query",
            "required": false,
            "description": "Pack D: <product id>,<rolls>,<price>. A custom roll is custom:<sheets>:<width>x<length>:<ply>.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "e",
            "in": "query",
            "required": false,
            "description": "Pack E: <product id>,<rolls>,<price>. A custom roll is custom:<sheets>:<width>x<length>:<ply>.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "f",
            "in": "query",
            "required": false,
            "description": "Pack F: <product id>,<rolls>,<price>. A custom roll is custom:<sheets>:<width>x<length>:<ply>.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "g",
            "in": "query",
            "required": false,
            "description": "Pack G: <product id>,<rolls>,<price>. A custom roll is custom:<sheets>:<width>x<length>:<ply>.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "h",
            "in": "query",
            "required": false,
            "description": "Pack H: <product id>,<rolls>,<price>. A custom roll is custom:<sheets>:<width>x<length>:<ply>.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Costs per pack and the winner. Cached for 5 minutes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Comparison"
                }
              }
            }
          },
          "400": {
            "description": "A parameter is missing or invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Product": {
        "type": "object",
        "required": [
          "id",
          "brand",
          "name",
          "sheetsPerRoll",
          "sheetWidthIn",
          "sheetLengthIn",
          "sqFtPerRoll",
          "ply",
          "isRegularRoll"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable product id, used in /api/compare and calculator links.",
            "examples": [
              "charmin-ultra-soft-mega"
            ]
          },
          "brand": {
            "type": "string",
            "examples": [
              "charmin"
            ]
          },
          "brandName": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Line and size, without the brand."
          },
          "fullName": {
            "type": "string"
          },
          "line": {
            "type": "string"
          },
          "size": {
            "type": [
              "string",
              "null"
            ],
            "description": "The brand's size name. Null when the brand has none (Kirkland Signature).",
            "examples": [
              "Mega",
              "Mega XL",
              "Super Mega"
            ]
          },
          "isRegularRoll": {
            "type": "boolean",
            "description": "The brand's regular roll, the yardstick for its \"= N regular\" claims. Not a pack you can compare."
          },
          "sheetsPerRoll": {
            "type": "integer"
          },
          "sheetWidthIn": {
            "type": "number"
          },
          "sheetLengthIn": {
            "type": "number"
          },
          "sqFtPerRoll": {
            "type": "number"
          },
          "ply": {
            "type": "integer"
          },
          "regularRolls": {
            "type": [
              "number",
              "null"
            ],
            "description": "What the pack says one roll equals in regular rolls. Null when the brand makes no such claim."
          },
          "regularRollSheets": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Sheets on this brand's regular roll. Null when the brand has none."
          },
          "honestRolls": {
            "type": "number",
            "description": "Square feet per roll ÷ 6."
          },
          "printedTotal": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "sqFt": {
                "type": "number"
              },
              "rolls": {
                "type": "integer"
              }
            }
          },
          "estimated": {
            "type": "object",
            "properties": {
              "sheets": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "size": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "note": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "sourceUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "url": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "softness": {
            "type": [
              "object",
              "null"
            ],
            "description": "Softness score for the product's line, 0 to 10. Null when not rated (no good source, a regular roll, or a custom roll). Method: https://toiletpaper.app/softness",
            "properties": {
              "score": {
                "type": "integer",
                "minimum": 0,
                "maximum": 10
              },
              "outOf": {
                "type": "integer",
                "const": 10
              },
              "line": {
                "type": "string"
              },
              "basis": {
                "type": "string",
                "description": "Why the line has this score."
              },
              "sources": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "publisher": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "date": {
                      "type": "string",
                      "description": "As precise as the source gives."
                    },
                    "dateKind": {
                      "type": "string",
                      "enum": [
                        "updated",
                        "published",
                        "tested",
                        "checked"
                      ],
                      "description": "What the date is: when the page was last updated or published, when the test ran, or when we checked an undated maker page."
                    },
                    "method": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "converterUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "problems": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "param": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Comparison": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "sheets",
              "area",
              "areaply"
            ]
          },
          "modeName": {
            "type": "string"
          },
          "priority": {
            "type": "string",
            "enum": [
              "cheap",
              "balanced",
              "soft"
            ]
          },
          "priorityName": {
            "type": "string"
          },
          "pickReason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "cheap",
              "none-rated",
              "cheapest-unrated",
              "balanced-cheap",
              "balanced-soft",
              "soft",
              null
            ],
            "description": "Why the winner won. none-rated and cheapest-unrated mean softness couldn't be weighed, so the cheapest won."
          },
          "packs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "pack": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "productId": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "sheetsPerRoll": {
                  "type": "number"
                },
                "sheetWidthIn": {
                  "type": "number"
                },
                "sheetLengthIn": {
                  "type": "number"
                },
                "ply": {
                  "type": "number"
                },
                "rolls": {
                  "type": "integer"
                },
                "price": {
                  "type": "number"
                },
                "totalSheets": {
                  "type": "number"
                },
                "totalSqFt": {
                  "type": "number"
                },
                "pricePer100Sheets": {
                  "type": "number"
                },
                "pricePerSqFt": {
                  "type": "number"
                },
                "pricePerSqFtPly": {
                  "type": "number"
                },
                "softness": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Softness score, 0 to 10, or null when not rated."
                },
                "best": {
                  "type": "boolean",
                  "description": "This pack wins for the priority p."
                },
                "cheapest": {
                  "type": "boolean",
                  "description": "This pack is the cheapest per unit (within 0.5%)."
                },
                "percentMoreThanBest": {
                  "type": "number",
                  "description": "Percent more per unit than the cheapest pack."
                }
              }
            }
          },
          "winner": {
            "type": [
              "string",
              "null"
            ],
            "description": "The winning pack's letter for the priority p, or null on a tie."
          },
          "tie": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "percentCheaperThanPriciest": {
            "type": "number"
          },
          "summary": {
            "type": "string"
          },
          "link": {
            "type": "string",
            "format": "uri",
            "description": "The same comparison in the calculator."
          }
        }
      }
    }
  }
}
