{
  "openapi": "3.1.0",
  "info": {
    "title": "Metrical Digital API",
    "summary": "Web performance audits and Core Web Vitals reports for any public URL.",
    "description": "HTTP endpoints served by metrical.digital.\n\nThese endpoints authenticate with the browser session: either a signed-in customer\nsession or the `metrical_guest_session_id` cookie issued on first visit. They back the\nMetrical web app.\n\nA standalone REST API with API-key authentication, scheduled scans, and webhooks is in\ndevelopment and is not described here. See https://metrical.digital/developers for status.\n\nEvery prose page on the site also has a Markdown representation, available via\n`Accept: text/markdown` content negotiation or by appending `.md` to the path.\n\nThis description is served from /openapi.json, /api/openapi.json, /swagger.json,\nand /.well-known/openapi.json. All four return the same document.\n\nEvery failure returns the `Error` schema as JSON, including unknown paths under\n/api, so a wrong URL never yields an HTML error page.\n\n## Versioning\n\nEvery endpoint is available under a version prefix, currently /api/v1. Breaking changes ship as a new prefix; the existing one keeps working.\n\n- The current version is v1, served from https://metrical.digital/api/v1.\n- Additive changes — new endpoints, new optional parameters, new response fields — ship within the current version. Treat unknown response fields as forwards-compatible and ignore them.\n- A change that would break an existing client ships as a new version prefix. The previous version keeps responding until its sunset date.\n- A version is never sunset with less than 180 days of notice.\n- Once deprecated, every response from that version carries a `Deprecation` header (RFC 9745), a `Sunset` header (RFC 8594), and a `Link` header with `rel=\"successor-version\"`.\n- The unversioned paths under https://metrical.digital/api are permanent aliases of the current version and move with it. Pin to a version prefix if that matters to you.\n- Every response names the version that produced it in the `API-Version` header.\n\nThe machine-readable form of this policy is served from https://metrical.digital/api.",
    "version": "1.0.0",
    "contact": {
      "name": "Metrical Digital support",
      "email": "richie@metrical.digital",
      "url": "https://metrical.digital/contact"
    },
    "termsOfService": "https://metrical.digital/terms"
  },
  "servers": [
    {
      "url": "https://metrical.digital",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Metrical Digital developer documentation",
    "url": "https://metrical.digital/developers"
  },
  "x-api-versioning": {
    "strategy": "url-path",
    "current_version": "v1",
    "current_base_url": "https://metrical.digital/api/v1",
    "unversioned_alias_url": "https://metrical.digital/api",
    "version_header": "API-Version",
    "deprecation_notice_days": 180,
    "deprecation_headers": [
      "Deprecation",
      "Sunset",
      "Link; rel=\"successor-version\""
    ],
    "specifications": [
      "RFC 9745 (Deprecation)",
      "RFC 8594 (Sunset)"
    ],
    "policy_url": "https://metrical.digital/developers#versioning",
    "versions": [
      {
        "version": "v1",
        "status": "current",
        "base_url": "https://metrical.digital/api/v1",
        "released": "2026-08-25",
        "deprecated": null,
        "sunset": null,
        "successor": null
      }
    ]
  },
  "tags": [
    {
      "name": "Scans",
      "description": "Retrieve and act on completed performance scans."
    },
    {
      "name": "Discovery",
      "description": "Machine-readable descriptions of the site and its content."
    }
  ],
  "security": [
    {
      "customerSession": []
    },
    {
      "guestSession": []
    }
  ],
  "paths": {
    "/api/v1/scan/{scanId}/report": {
      "get": {
        "tags": [
          "Scans"
        ],
        "operationId": "getScanReport",
        "summary": "Export a completed scan report",
        "description": "Returns the report for a scan the current session owns, rendered as CSV, JSON, or Markdown.",
        "parameters": [
          {
            "name": "scanId",
            "in": "path",
            "required": true,
            "description": "Identifier of a completed scan.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Report format. Unrecognised values fall back to `csv`.",
            "schema": {
              "type": "string",
              "enum": [
                "csv",
                "json",
                "markdown"
              ],
              "default": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The rendered report.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No customer or guest session was presented.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such scan for this session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scans/{scanId}/feedback": {
      "post": {
        "tags": [
          "Scans"
        ],
        "operationId": "submitScanFeedback",
        "summary": "Submit feedback on a scan result",
        "parameters": [
          {
            "name": "scanId",
            "in": "path",
            "required": true,
            "description": "Identifier of the scan the feedback relates to.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanFeedback"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feedback recorded.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            }
          },
          "400": {
            "description": "The request body was not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/scan/{scanId}/report": {
      "get": {
        "tags": [
          "Scans"
        ],
        "operationId": "getScanReportUnversioned",
        "summary": "Export a completed scan report",
        "description": "Unversioned alias of `GET /api/v1/scan/{scanId}/report`. Tracks the current version; pin to the versioned path if that matters to you.",
        "parameters": [
          {
            "name": "scanId",
            "in": "path",
            "required": true,
            "description": "Identifier of a completed scan.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Report format. Unrecognised values fall back to `csv`.",
            "schema": {
              "type": "string",
              "enum": [
                "csv",
                "json",
                "markdown"
              ],
              "default": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The rendered report.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No customer or guest session was presented.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such scan for this session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/scans/{scanId}/feedback": {
      "post": {
        "tags": [
          "Scans"
        ],
        "operationId": "submitScanFeedbackUnversioned",
        "summary": "Submit feedback on a scan result",
        "description": "Unversioned alias of `POST /api/v1/scans/{scanId}/feedback`.",
        "parameters": [
          {
            "name": "scanId",
            "in": "path",
            "required": true,
            "description": "Identifier of the scan the feedback relates to.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanFeedback"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feedback recorded.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            }
          },
          "400": {
            "description": "The request body was not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "getApiIndex",
        "summary": "API index: versions, endpoints, and the deprecation policy",
        "description": "The machine-readable entry point. Lists every version with its lifecycle state, the endpoints in the current version, and where the specification and documentation live.",
        "security": [],
        "responses": {
          "200": {
            "description": "The API index.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/{version}": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "getApiVersion",
        "summary": "Version document: lifecycle state and endpoints",
        "description": "Once a version is deprecated, this response carries `Deprecation` (RFC 9745), `Sunset` (RFC 8594), and `Link: rel=\"successor-version\"`.",
        "security": [],
        "parameters": [
          {
            "name": "version",
            "in": "path",
            "required": true,
            "description": "API version identifier.",
            "schema": {
              "type": "string",
              "enum": [
                "v1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The version document.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiVersion"
                }
              }
            }
          },
          "404": {
            "description": "No such version.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "getLlmsTxt",
        "summary": "Curated index of the site for language models",
        "description": "Follows the llms.txt format described at https://llmstxt.org.",
        "security": [],
        "responses": {
          "200": {
            "description": "The llms.txt index.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/llms-full.txt": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "getLlmsFullTxt",
        "summary": "Every public page as Markdown, concatenated",
        "security": [],
        "responses": {
          "200": {
            "description": "The full content corpus.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "getOpenApiDocument",
        "summary": "This document",
        "security": [],
        "responses": {
          "200": {
            "description": "The OpenAPI description of this API.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "ApiVersion": {
        "description": "The API version that produced this response.",
        "schema": {
          "type": "string",
          "examples": [
            "v1"
          ]
        }
      },
      "Deprecation": {
        "description": "RFC 9745. Present once this version is deprecated: `@` followed by seconds since the epoch.",
        "required": false,
        "schema": {
          "type": "string",
          "examples": [
            "@1782000000"
          ]
        }
      },
      "Sunset": {
        "description": "RFC 8594. The date this version stops responding. Present once one is scheduled.",
        "required": false,
        "schema": {
          "type": "string",
          "examples": [
            "Sat, 31 Oct 2026 00:00:00 GMT"
          ]
        }
      }
    },
    "securitySchemes": {
      "customerSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__session",
        "description": "Clerk session cookie set when a customer signs in."
      },
      "guestSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "metrical_guest_session_id",
        "description": "Anonymous session cookie issued on first visit, scoping guest scans."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every API failure returns this shape. `error.code` is stable and safe to branch on; `error.hint` describes how to recover.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, machine-readable error identifier.",
                "enum": [
                  "not_found",
                  "method_not_allowed",
                  "unauthorized",
                  "invalid_request",
                  "quota_exceeded",
                  "upstream_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "Human-readable failure reason."
              },
              "hint": {
                "type": "string",
                "description": "How to recover from this error."
              },
              "status": {
                "type": "integer",
                "description": "HTTP status code, repeated in the body."
              },
              "documentation_url": {
                "type": "string",
                "format": "uri",
                "description": "Human documentation for this API."
              },
              "specification_url": {
                "type": "string",
                "format": "uri",
                "description": "This OpenAPI description."
              }
            },
            "required": [
              "code",
              "message",
              "hint",
              "status",
              "documentation_url",
              "specification_url"
            ]
          },
          "message": {
            "type": "string",
            "description": "Copy of `error.message`, kept for backwards compatibility."
          }
        },
        "required": [
          "error",
          "message"
        ]
      },
      "ApiVersion": {
        "type": "object",
        "description": "The lifecycle state of one API version.",
        "properties": {
          "version": {
            "type": "string",
            "description": "Version identifier, used as the URL path prefix."
          },
          "status": {
            "type": "string",
            "enum": [
              "current",
              "deprecated",
              "sunset"
            ],
            "description": "Where this version is in its lifecycle."
          },
          "base_url": {
            "type": "string",
            "format": "uri",
            "description": "Base URL for endpoints in it."
          },
          "released": {
            "type": "string",
            "format": "date"
          },
          "deprecated": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "When the deprecation was announced, or null."
          },
          "sunset": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "When it stops responding, or null."
          },
          "successor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Base URL of the version that replaces it, or null."
          }
        },
        "required": [
          "version",
          "status",
          "base_url",
          "released",
          "deprecated",
          "sunset",
          "successor"
        ]
      },
      "ScanFeedback": {
        "type": "object",
        "description": "Feedback on the usefulness of a scan result.",
        "properties": {
          "rating": {
            "type": "string",
            "description": "Whether the result was helpful.",
            "enum": [
              "up",
              "down"
            ]
          },
          "comment": {
            "type": "string",
            "description": "Optional free-text comment."
          }
        }
      }
    }
  }
}
