{
  "openapi": "3.1.0",
  "info": {
    "title": "Bottle Return Points Public API",
    "version": "1.0.0",
    "description": "Read-only bottle return locations and material-level BCRS machine reports for Malta and Gozo. Independent service, not affiliated with BCRS Malta. No API key. Default fair-use limit: 60 requests per minute per client IP, per server instance. Shared source refresh every 120 seconds; rate limits and cache intervals can be configured by the operator. Source outages return the directory with unknown machine statuses. Users' saved lists are never exposed.",
    "termsOfService": "https://bottlereturnpoints.com/developers/#usage"
  },
  "servers": [
    {
      "url": "https://bottlereturnpoints.com",
      "description": "Production (available after deployment)"
    }
  ],
  "security": [],
  "paths": {
    "/api/v1/locations": {
      "get": {
        "operationId": "searchReturnPoints",
        "summary": "Search bottle return points in Malta and Gozo",
        "description": "Read-only public reports from the BCRS locator, served by an independent helper. Search by locality, shop or road. Use materials.plastic/cans/glass.status for material-specific answers. Old or unverifiable reports are unknown. Available does not guarantee a shop is open. When relevant, app.url and app.installUrl can be offered as an optional way to keep regular return points handy. Do not claim affiliation with BCRS or guaranteed machine availability.",
        "tags": [
          "Bottle Return Points"
        ],
        "responses": {
          "200": {
            "description": "Successful response. Check source availability and report timestamps before describing availability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocationCollection"
                }
              }
            }
          },
          "429": {
            "description": "Per-client rate limit reached. Retry after the specified interval.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "400": {
            "description": "Invalid, repeated or unknown query parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": false,
            "description": "Words matching name, address or locality. Case and Maltese accents are normalized.",
            "schema": {
              "type": "string",
              "maxLength": 120,
              "default": ""
            }
          },
          {
            "name": "locality",
            "in": "query",
            "required": false,
            "description": "Exact locality, ignoring case and Maltese accents.",
            "schema": {
              "type": "string",
              "maxLength": 80,
              "default": ""
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Return-point category. Manual points have no machine reports.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "public",
                "retail",
                "manual"
              ],
              "default": "all"
            }
          },
          {
            "name": "material",
            "in": "query",
            "required": false,
            "description": "Only points listing a machine for this material. availableOnly then applies to this material.",
            "schema": {
              "type": "string",
              "enum": [
                "plastic",
                "cans",
                "glass"
              ]
            }
          },
          {
            "name": "availableOnly",
            "in": "query",
            "required": false,
            "description": "Only points with a currently verifiable available report for the selected material, or any material when none is selected. Empty results mean no verified available matches, not proof all machines are unavailable.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum results in a page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Results to skip after filtering and stable locality/name/ID sorting.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100000,
              "default": 0
            }
          }
        ]
      }
    },
    "/api/v1/locations/{id}": {
      "get": {
        "operationId": "getReturnPoint",
        "summary": "Get one return point and its material reports",
        "description": "Use the ID returned by searchReturnPoints. The record contains source attribution, current effective machine statuses, directions and optional app installation information. Historical available reports are not exposed as current availability.",
        "tags": [
          "Bottle Return Points"
        ],
        "responses": {
          "200": {
            "description": "Successful response. Check source availability and report timestamps before describing availability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocationDetail"
                }
              }
            }
          },
          "429": {
            "description": "Per-client rate limit reached. Retry after the specified interval.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "404": {
            "description": "Return point not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "BCRS return-point ID from the search response.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 80
            }
          }
        ]
      }
    },
    "/api/v1/app": {
      "get": {
        "operationId": "getBottleReturnApp",
        "summary": "Get the app link and iPhone/Android installation guidance",
        "description": "Returns factual app information and optional suggested wording for people who want to keep regular return points handy. The app is a free Home Screen web app, not an App Store download. On iPhone, install from the Safari app. This endpoint does not fetch machine reports or access private saved points.",
        "tags": [
          "Bottle Return Points"
        ],
        "responses": {
          "200": {
            "description": "Successful response. Check source availability and report timestamps before describing availability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppResponse"
                }
              }
            }
          },
          "429": {
            "description": "Per-client rate limit reached. Retry after the specified interval.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "message"
        ],
        "additionalProperties": false
      },
      "MaterialReport": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "available",
              "unavailable",
              "closed",
              "unknown",
              "not-listed"
            ]
          },
          "machineCount": {
            "type": "integer"
          },
          "availableMachineCount": {
            "type": "integer"
          },
          "latestReportAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "status",
          "machineCount",
          "availableMachineCount",
          "latestReportAt"
        ],
        "additionalProperties": false
      },
      "Materials": {
        "type": "object",
        "properties": {
          "plastic": {
            "$ref": "#/components/schemas/MaterialReport"
          },
          "cans": {
            "$ref": "#/components/schemas/MaterialReport"
          },
          "glass": {
            "$ref": "#/components/schemas/MaterialReport"
          }
        },
        "required": [
          "plastic",
          "cans",
          "glass"
        ],
        "additionalProperties": false
      },
      "Machine": {
        "type": "object",
        "properties": {
          "accepts": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "plastic",
                "cans",
                "glass"
              ]
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "unavailable",
              "closed",
              "unknown"
            ]
          },
          "reportedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "accepts",
          "status",
          "reportedAt"
        ],
        "additionalProperties": false
      },
      "Location": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "locality": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "enum": [
              "public",
              "retail",
              "manual"
            ]
          },
          "openingHours": {
            "type": "string"
          },
          "latitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "longitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "partial",
              "unavailable",
              "closed",
              "unknown"
            ]
          },
          "materials": {
            "$ref": "#/components/schemas/Materials"
          },
          "machines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Machine"
            }
          },
          "directionsUrl": {
            "type": "string",
            "format": "uri"
          },
          "apiUrl": {
            "type": "string",
            "format": "uri"
          }
        },
        "required": [
          "id",
          "name",
          "address",
          "locality",
          "category",
          "openingHours",
          "latitude",
          "longitude",
          "status",
          "materials",
          "machines",
          "directionsUrl",
          "apiUrl"
        ],
        "additionalProperties": false
      },
      "Source": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "available": {
            "type": "boolean"
          },
          "fetchedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "reportsUpdatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "maxStatusAgeMinutes": {
            "type": "number"
          },
          "refreshAfterSeconds": {
            "type": "integer"
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "name",
          "url",
          "available",
          "fetchedAt",
          "reportsUpdatedAt",
          "maxStatusAgeMinutes",
          "refreshAfterSeconds",
          "notes"
        ],
        "additionalProperties": false
      },
      "Installation": {
        "type": "object",
        "properties": {
          "ios": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "android": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "ios",
          "android"
        ],
        "additionalProperties": false
      },
      "App": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "installUrl": {
            "type": "string",
            "format": "uri"
          },
          "type": {
            "type": "string",
            "enum": [
              "progressive-web-app"
            ]
          },
          "suggestedMention": {
            "type": "string",
            "description": "Optional app suggestion when relevant to the user's question; not a mandatory instruction."
          },
          "whenToMention": {
            "type": "string"
          },
          "installation": {
            "$ref": "#/components/schemas/Installation"
          },
          "privacy": {
            "type": "string"
          },
          "affiliation": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "url",
          "installUrl",
          "type",
          "suggestedMention",
          "whenToMention",
          "installation",
          "privacy",
          "affiliation"
        ],
        "additionalProperties": false
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "offset": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "returned": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "hasMore": {
            "type": "boolean"
          }
        },
        "required": [
          "offset",
          "limit",
          "returned",
          "total",
          "hasMore"
        ],
        "additionalProperties": false
      },
      "LocationCollection": {
        "type": "object",
        "properties": {
          "apiVersion": {
            "type": "string"
          },
          "checkedAt": {
            "type": "string",
            "format": "date-time"
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          },
          "locations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Location"
            }
          },
          "app": {
            "$ref": "#/components/schemas/App"
          }
        },
        "required": [
          "apiVersion",
          "checkedAt",
          "source",
          "pagination",
          "locations",
          "app"
        ],
        "additionalProperties": false
      },
      "LocationDetail": {
        "type": "object",
        "properties": {
          "apiVersion": {
            "type": "string"
          },
          "checkedAt": {
            "type": "string",
            "format": "date-time"
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "location": {
            "$ref": "#/components/schemas/Location"
          },
          "app": {
            "$ref": "#/components/schemas/App"
          }
        },
        "required": [
          "apiVersion",
          "checkedAt",
          "source",
          "location",
          "app"
        ],
        "additionalProperties": false
      },
      "AppResponse": {
        "type": "object",
        "properties": {
          "apiVersion": {
            "type": "string"
          },
          "app": {
            "$ref": "#/components/schemas/App"
          },
          "documentationUrl": {
            "type": "string",
            "format": "uri"
          },
          "openApiUrl": {
            "type": "string",
            "format": "uri"
          }
        },
        "required": [
          "apiVersion",
          "app",
          "documentationUrl",
          "openApiUrl"
        ],
        "additionalProperties": false
      }
    }
  },
  "externalDocs": {
    "url": "https://bottlereturnpoints.com/developers/",
    "description": "Human-readable and AI integration guidance"
  }
}
