{
  "openapi": "3.1.0",
  "info": {
    "title": "ScopeMatch API",
    "version": "1.0.0",
    "description": "Public JSON API for accessing ScopeMatch's directory of ISO 17025 accredited testing laboratories across Europe.\n\n## Authentication\n\nPass your API key via the `X-Api-Key` header (recommended) or the `api_key` query parameter. Anonymous access is allowed with lower rate limits.\n\n## Tiers\n\n| Tier | Daily Limit | Burst/min | Geo Coordinates | Capabilities |\n|------|-------------|-----------|-----------------|--------------|\n| Anonymous | 100 | 10 | No | No |\n| Free | 1,000 | 60 | No | No |\n| Pro | 10,000 | 200 | Yes | Yes |\n| Enterprise | 100,000 | 500 | Yes | Yes |\n\n## Rate Limiting\n\nAll responses include rate limit headers:\n- `X-RateLimit-Limit` — Burst limit (requests per minute)\n- `X-RateLimit-Remaining` — Requests remaining this minute\n- `X-RateLimit-Reset` — Unix timestamp when the burst window resets\n- `X-RateLimit-Daily-Limit` — Daily quota\n- `X-RateLimit-Daily-Remaining` — Requests remaining today\n- `X-Api-Tier` — Your resolved tier (`anonymous`, `free`, `pro`, `enterprise`)\n\n## Attribution\n\nAll tiers require attribution. Include a visible link to ScopeMatch when displaying data from this API.",
    "contact": {
      "name": "ScopeMatch",
      "url": "https://scopematch.eu/labs/api-docs/"
    },
    "termsOfService": "https://scopematch.eu/labs/api-docs/terms"
  },
  "servers": [
    {
      "url": "https://scopematch.eu/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {},
    { "ApiKeyHeader": [] },
    { "ApiKeyQuery": [] }
  ],
  "paths": {
    "/labs": {
      "get": {
        "operationId": "listLabs",
        "summary": "List laboratories",
        "description": "Returns a paginated list of ISO 17025 accredited testing laboratories. Supports filtering by country, accreditation body, category, search query, and standard reference.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "description": "Filter by ISO 3166-1 alpha-2 country code (e.g. `DE`, `GB`, `FR`).",
            "schema": { "type": "string", "example": "DE" }
          },
          {
            "name": "body",
            "in": "query",
            "description": "Filter by accreditation body code (e.g. `DAkkS`, `UKAS`, `COFRAC`). Case-insensitive.",
            "schema": { "type": "string", "example": "DAkkS" }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Filter by category slug (e.g. `mechanical-testing`, `calibration`).",
            "schema": { "type": "string", "example": "mechanical-testing" }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Full-text search across lab name, city, capability names, canonical IDs, and standard references.",
            "schema": { "type": "string", "example": "tensile" }
          },
          {
            "name": "standard",
            "in": "query",
            "description": "Filter by standard reference (e.g. `ISO 6892-1`). Automatically normalized for matching.",
            "schema": { "type": "string", "example": "ISO 6892-1" }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by accreditation status.",
            "schema": {
              "type": "string",
              "enum": ["active", "suspended", "withdrawn", "all"],
              "default": "active"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-indexed).",
            "schema": { "type": "integer", "minimum": 1, "default": 1 }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Results per page.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of labs.",
            "headers": {
              "X-Api-Tier": { "$ref": "#/components/headers/X-Api-Tier" },
              "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/X-RateLimit-Remaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" },
              "X-RateLimit-Daily-Limit": { "$ref": "#/components/headers/X-RateLimit-Daily-Limit" },
              "X-RateLimit-Daily-Remaining": { "$ref": "#/components/headers/X-RateLimit-Daily-Remaining" }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/LabListItem" }
                    },
                    "meta": { "$ref": "#/components/schemas/PaginationMeta" },
                    "_links": { "$ref": "#/components/schemas/PaginationLinks" }
                  },
                  "required": ["data", "meta", "_links"]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/labs/{slug}": {
      "get": {
        "operationId": "getLab",
        "summary": "Get lab detail",
        "description": "Returns detailed information for a single laboratory, including contact details and scope document URL. Pro and Enterprise tiers also receive capabilities (test methods) and geo coordinates.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "URL-safe lab identifier.",
            "schema": { "type": "string", "example": "ptb-braunschweig" }
          }
        ],
        "responses": {
          "200": {
            "description": "Lab detail.",
            "headers": {
              "X-Api-Tier": { "$ref": "#/components/headers/X-Api-Tier" },
              "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/X-RateLimit-Remaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" },
              "X-RateLimit-Daily-Limit": { "$ref": "#/components/headers/X-RateLimit-Daily-Limit" },
              "X-RateLimit-Daily-Remaining": { "$ref": "#/components/headers/X-RateLimit-Daily-Remaining" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LabDetail" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/metadata/freshness": {
      "get": {
        "operationId": "getDataFreshness",
        "summary": "Data freshness",
        "description": "Shows when each accreditation body was last scraped, how many labs it contains, and the overall total. No API key required. Response is cached for 1 hour.",
        "security": [],
        "responses": {
          "200": {
            "description": "Data freshness per accreditation body.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/FreshnessResponse" }
              }
            }
          }
        }
      }
    },
    "/categories": {
      "get": {
        "operationId": "listCategories",
        "summary": "List categories",
        "description": "Returns all top-level test categories with the count of labs in each.",
        "responses": {
          "200": {
            "description": "Category listing.",
            "headers": {
              "X-Api-Tier": { "$ref": "#/components/headers/X-Api-Tier" },
              "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/X-RateLimit-Remaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" },
              "X-RateLimit-Daily-Limit": { "$ref": "#/components/headers/X-RateLimit-Daily-Limit" },
              "X-RateLimit-Daily-Remaining": { "$ref": "#/components/headers/X-RateLimit-Daily-Remaining" }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/CategoryItem" }
                    }
                  },
                  "required": ["data"]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Pass your API key in the `X-Api-Key` header (recommended)."
      },
      "ApiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "api_key",
        "description": "Pass your API key as a query parameter (less secure — may appear in logs)."
      }
    },
    "headers": {
      "X-Api-Tier": {
        "description": "Your resolved API tier (`anonymous`, `free`, `pro`, or `enterprise`).",
        "schema": { "type": "string", "enum": ["anonymous", "free", "pro", "enterprise"] }
      },
      "X-RateLimit-Limit": {
        "description": "Burst rate limit (requests per minute).",
        "schema": { "type": "integer" }
      },
      "X-RateLimit-Remaining": {
        "description": "Burst requests remaining in the current minute window.",
        "schema": { "type": "integer" }
      },
      "X-RateLimit-Reset": {
        "description": "Unix timestamp when the current burst window resets.",
        "schema": { "type": "integer" }
      },
      "X-RateLimit-Daily-Limit": {
        "description": "Daily request quota for your tier.",
        "schema": { "type": "integer" }
      },
      "X-RateLimit-Daily-Remaining": {
        "description": "Daily requests remaining (resets at UTC midnight).",
        "schema": { "type": "integer" }
      }
    },
    "schemas": {
      "LabListItem": {
        "type": "object",
        "description": "Compact lab representation used in list responses.",
        "properties": {
          "slug": { "type": "string", "description": "URL-safe lab identifier." },
          "name": { "type": "string", "description": "Display name of the laboratory." },
          "accreditation_number": { "type": ["string", "null"], "description": "Accreditation certificate number." },
          "accreditation_body": { "type": ["string", "null"], "description": "Accreditation body code (e.g. `DAkkS`, `UKAS`)." },
          "accreditation_status": { "type": ["string", "null"], "description": "Current status.", "enum": ["active", "suspended", "withdrawn", null] },
          "country": { "type": "string", "description": "ISO 3166-1 alpha-2 country code." },
          "city": { "type": ["string", "null"], "description": "City name." },
          "postcode": { "type": ["string", "null"], "description": "Postal code." },
          "latitude": { "type": ["number", "null"], "description": "Latitude. **Pro and Enterprise tiers only** — returns `null` for Free and Anonymous." },
          "longitude": { "type": ["number", "null"], "description": "Longitude. **Pro and Enterprise tiers only** — returns `null` for Free and Anonymous." },
          "categories": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Category slugs for this lab's capabilities."
          },
          "_links": { "$ref": "#/components/schemas/LabLinks" }
        },
        "required": ["slug", "name", "country", "categories", "_links"]
      },
      "LabDetail": {
        "type": "object",
        "description": "Full lab detail. Pro/Enterprise tiers receive additional fields (coordinates, capabilities).",
        "properties": {
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "accreditation_number": { "type": ["string", "null"] },
          "accreditation_body": { "type": ["string", "null"] },
          "accreditation_status": { "type": ["string", "null"], "enum": ["active", "suspended", "withdrawn", null] },
          "country": { "type": "string" },
          "city": { "type": ["string", "null"] },
          "postcode": { "type": ["string", "null"] },
          "street_address": { "type": ["string", "null"], "description": "Street address." },
          "phone": { "type": ["string", "null"], "description": "Phone number." },
          "email": { "type": ["string", "null"], "description": "Business contact email (personal emails are filtered)." },
          "website_url": { "type": ["string", "null"], "description": "Lab website URL." },
          "scope_document_url": { "type": ["string", "null"], "description": "URL to the original accreditation scope document." },
          "accreditation_date": { "type": ["string", "null"], "format": "date", "description": "Date of accreditation (ISO 8601)." },
          "last_verified": { "type": ["string", "null"], "format": "date", "description": "Date the record was last verified against the source." },
          "latitude": { "type": ["number", "null"], "description": "Latitude. **Pro and Enterprise tiers only** — returns `null` for Free and Anonymous." },
          "longitude": { "type": ["number", "null"], "description": "Longitude. **Pro and Enterprise tiers only** — returns `null` for Free and Anonymous." },
          "categories": {
            "type": "array",
            "items": { "type": "string" }
          },
          "capabilities": {
            "type": ["array", "null"],
            "items": { "$ref": "#/components/schemas/CapabilityItem" },
            "description": "Test method capabilities. **Pro and Enterprise tiers only** — returns `null` for Free and Anonymous."
          },
          "_links": { "$ref": "#/components/schemas/LabLinks" },
          "_meta": {
            "type": "object",
            "description": "Present only for Free and Anonymous tiers when capabilities are not included.",
            "properties": {
              "capabilities_available": { "type": "boolean", "description": "Always `false` when this block is present." },
              "upgrade_url": { "type": "string", "description": "URL to upgrade for capability access." }
            }
          }
        },
        "required": ["slug", "name", "country", "categories", "_links"]
      },
      "CapabilityItem": {
        "type": "object",
        "description": "A test method or calibration capability.",
        "properties": {
          "canonical_id": { "type": "string", "description": "Normalized capability identifier (e.g. `TEST-TENSILE-METAL-ISO6892-1`)." },
          "name": { "type": "string", "description": "Human-readable capability name." },
          "category_slug": { "type": ["string", "null"], "description": "Parent category slug." },
          "category_name": { "type": ["string", "null"], "description": "Parent category name." }
        },
        "required": ["canonical_id", "name"]
      },
      "CategoryItem": {
        "type": "object",
        "description": "A top-level test category with lab count.",
        "properties": {
          "slug": { "type": "string", "description": "URL-safe category identifier." },
          "name": { "type": "string", "description": "Human-readable category name." },
          "lab_count": { "type": "integer", "description": "Number of labs with capabilities in this category." },
          "_links": {
            "type": "object",
            "properties": {
              "labs": { "type": "string", "description": "API URL to list labs in this category." },
              "html": { "type": "string", "description": "Web URL to search labs in this category." }
            }
          }
        },
        "required": ["slug", "name", "lab_count", "_links"]
      },
      "FreshnessResponse": {
        "type": "object",
        "description": "Data freshness metadata per accreditation body.",
        "properties": {
          "bodies": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "country": { "type": ["string", "null"], "description": "ISO 3166-1 alpha-2 country code." },
                "last_scraped": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of last successful scrape." },
                "labs_count": { "type": "integer", "description": "Number of labs from this body." },
                "age_days": { "type": "integer", "description": "Days since last scrape." }
              },
              "required": ["last_scraped", "labs_count", "age_days"]
            }
          },
          "total_labs": { "type": "integer", "description": "Total labs across all bodies." },
          "generated_at": { "type": "string", "format": "date-time", "description": "When this response was generated." }
        },
        "required": ["bodies", "total_labs", "generated_at"]
      },
      "LabLinks": {
        "type": "object",
        "properties": {
          "self": { "type": "string", "description": "API URL for this lab." },
          "html": { "type": "string", "description": "Web page URL for this lab." }
        }
      },
      "PaginationMeta": {
        "type": "object",
        "properties": {
          "page": { "type": "integer", "description": "Current page number." },
          "per_page": { "type": "integer", "description": "Results per page." },
          "total": { "type": "integer", "description": "Total number of matching results." },
          "total_pages": { "type": "integer", "description": "Total number of pages." }
        },
        "required": ["page", "per_page", "total", "total_pages"]
      },
      "PaginationLinks": {
        "type": "object",
        "properties": {
          "self": { "type": "string", "description": "URL for the current page." },
          "next": { "type": ["string", "null"], "description": "URL for the next page, or `null` if on the last page." },
          "prev": { "type": ["string", "null"], "description": "URL for the previous page, or `null` if on the first page." }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "description": "Error message." },
          "docs": { "type": "string", "description": "Link to API documentation." },
          "retry_after": { "type": "integer", "description": "Seconds until the rate limit resets (429 responses only)." },
          "upgrade_url": { "type": "string", "description": "URL to upgrade your tier (429 responses only)." }
        },
        "required": ["error"]
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Invalid or inactive API key.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": "Invalid or inactive API key.",
              "docs": "https://scopematch.eu/labs/api-docs/"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": "Lab not found"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the rate limit resets.",
            "schema": { "type": "integer" }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": "Rate limit exceeded.",
              "retry_after": 60,
              "upgrade_url": "https://scopematch.eu/labs/api-docs/#pricing"
            }
          }
        }
      }
    }
  }
}
