{
    "openapi": "3.0.3",
    "info": {
        "title": "binable API",
        "version": "1.0.0",
        "description": "API zur Abfrage von Müllabfuhrterminen für Adressen in Deutschland und Österreich.",
        "contact": {
            "url": "https:\/\/binable.app"
        },
        "license": {
            "name": "Proprietary"
        }
    },
    "servers": [
        {
            "url": "https:\/\/binable.app",
            "description": "Production"
        }
    ],
    "security": [
        {
            "ApiKey": []
        }
    ],
    "components": {
        "securitySchemes": {
            "ApiKey": {
                "type": "apiKey",
                "in": "header",
                "name": "Authorization",
                "description": "API-Schlüssel im Format: `ApiKey \u003Cdein-schlüssel\u003E`. Kostenlos via E-Mail-Registrierung erhältlich."
            }
        },
        "schemas": {
            "Address": {
                "type": "object",
                "required": [
                    "street",
                    "houseNumber",
                    "zip",
                    "city",
                    "country"
                ],
                "properties": {
                    "street": {
                        "type": "string",
                        "minLength": 3,
                        "maxLength": 255,
                        "example": "Musterstraße"
                    },
                    "houseNumber": {
                        "type": "string",
                        "maxLength": 10,
                        "example": "1a"
                    },
                    "zip": {
                        "type": "string",
                        "maxLength": 10,
                        "example": "12345"
                    },
                    "city": {
                        "type": "string",
                        "minLength": 3,
                        "maxLength": 50,
                        "example": "Musterstadt"
                    },
                    "country": {
                        "type": "string",
                        "minLength": 2,
                        "maxLength": 2,
                        "example": "DE",
                        "description": "ISO 3166-1 alpha-2 Ländercode."
                    }
                }
            },
            "Position": {
                "type": "object",
                "required": [
                    "latitude",
                    "longitude"
                ],
                "properties": {
                    "latitude": {
                        "type": "number",
                        "format": "double",
                        "minimum": -90,
                        "maximum": 90,
                        "example": 52.520008,
                        "description": "Geografische Breite (WGS84). Alternativ als \u0022lat\u0022."
                    },
                    "longitude": {
                        "type": "number",
                        "format": "double",
                        "minimum": -180,
                        "maximum": 180,
                        "example": 13.404954,
                        "description": "Geografische Länge (WGS84). Alternativ als \u0022lng\u0022 oder \u0022lon\u0022."
                    }
                }
            },
            "CollectionDates": {
                "type": "object",
                "properties": {
                    "dates": {
                        "type": "array",
                        "items": {
                            "type": "string",
                            "format": "date"
                        },
                        "example": [
                            "2025-01-15",
                            "2025-01-29",
                            "2025-02-12"
                        ]
                    },
                    "next": {
                        "type": "string",
                        "format": "date",
                        "nullable": true,
                        "example": "2025-01-15"
                    },
                    "nextDays": {
                        "type": "string",
                        "nullable": true,
                        "example": "7",
                        "description": "Tage bis zur nächsten Leerung als String."
                    },
                    "last": {
                        "type": "string",
                        "format": "date",
                        "nullable": true,
                        "example": "2024-12-30"
                    }
                }
            },
            "ProviderInformation": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "example": "AWB Landkreis Musterstadt"
                    },
                    "uri": {
                        "type": "string",
                        "format": "uri",
                        "example": "https:\/\/awb-musterstadt.de"
                    }
                }
            },
            "FetchResult": {
                "type": "object",
                "properties": {
                    "address": {
                        "$ref": "#\/components\/schemas\/Address"
                    },
                    "provider": {
                        "$ref": "#\/components\/schemas\/ProviderInformation"
                    },
                    "paper": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "bio": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "residualWaste": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "residualWasteLessFrequent": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "residualWasteContainer": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "residualWasteContainerLessFrequent": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "reusableMaterials": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "glassContainer": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "collectionStation": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "christmasTree": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "toxic": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "diaper": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "hedgeTreeTrimming": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    },
                    "fleaMarket": {
                        "$ref": "#\/components\/schemas\/CollectionDates"
                    }
                }
            },
            "DailyStat": {
                "type": "object",
                "properties": {
                    "date": {
                        "type": "string",
                        "format": "date",
                        "example": "2025-01-10"
                    },
                    "success": {
                        "type": "integer",
                        "example": 42
                    },
                    "failed": {
                        "type": "integer",
                        "example": 3
                    }
                }
            },
            "HealthResult": {
                "type": "object",
                "properties": {
                    "providerCount": {
                        "type": "integer",
                        "example": 150
                    },
                    "stats": {
                        "type": "array",
                        "items": {
                            "$ref": "#\/components\/schemas\/DailyStat"
                        }
                    }
                }
            },
            "WebhookCreateRequest": {
                "type": "object",
                "required": [
                    "url",
                    "street",
                    "houseNumber",
                    "zip",
                    "city",
                    "country"
                ],
                "properties": {
                    "url": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 1024,
                        "example": "https:\/\/mein-server.example\/webhook",
                        "description": "Ziel-URL, an die binable den HTTP-POST sendet."
                    },
                    "daysBeforeCollection": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 7,
                        "default": 1,
                        "example": 1,
                        "description": "Wie viele Tage vor der Abfuhr die Benachrichtigung ausgelöst wird."
                    },
                    "street": {
                        "type": "string",
                        "example": "Musterstraße"
                    },
                    "houseNumber": {
                        "type": "string",
                        "example": "1a"
                    },
                    "zip": {
                        "type": "string",
                        "example": "12345"
                    },
                    "city": {
                        "type": "string",
                        "example": "Musterstadt"
                    },
                    "country": {
                        "type": "string",
                        "example": "DE"
                    }
                }
            },
            "WebhookResponse": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "format": "uuid",
                        "example": "018f1e2a-3b4c-7d5e-8f9a-0b1c2d3e4f56",
                        "description": "UUID des Webhooks — wird für DELETE \/api\/webhook\/{id} benötigt."
                    },
                    "url": {
                        "type": "string",
                        "example": "https:\/\/mein-server.example\/webhook"
                    },
                    "secret": {
                        "type": "string",
                        "example": "a3f8b1c2d4e5...",
                        "description": "Zufälliges Secret für die HMAC-SHA256-Signatur. Wird nur einmal bei der Erstellung zurückgegeben."
                    },
                    "daysBeforeCollection": {
                        "type": "integer",
                        "example": 1
                    },
                    "active": {
                        "type": "boolean",
                        "example": true
                    }
                }
            },
            "ApiKeyInfo": {
                "type": "object",
                "properties": {
                    "email": {
                        "type": "string",
                        "format": "email",
                        "example": "ich@example.org",
                        "description": "E-Mail-Adresse, mit der der API-Schlüssel registriert wurde."
                    },
                    "created": {
                        "type": "string",
                        "format": "date-time",
                        "example": "2025-01-15T10:23:44+00:00",
                        "description": "Zeitpunkt der Registrierung."
                    },
                    "confirmDate": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true,
                        "example": "2025-01-15T10:25:01+00:00",
                        "description": "Zeitpunkt der E-Mail-Bestätigung."
                    }
                }
            },
            "ErrorResponse": {
                "type": "object",
                "properties": {
                    "error": {
                        "type": "string",
                        "example": "Provider found, but no result for: Musterstraße 1a, 12345 Musterstadt, DE"
                    }
                }
            }
        }
    },
    "paths": {
        "\/api\/fetch": {
            "post": {
                "summary": "Abfuhrtermine abrufen",
                "description": "Liefert alle Abfuhrtermine für eine Adresse gruppiert nach Abfallart. Ein API-Schlüssel ist optional — anonyme Anfragen sind möglich, unterliegen aber ggf. strengeren Rate-Limits.",
                "operationId": "fetchGarbageCollectionDates",
                "tags": [
                    "Abfuhrtermine"
                ],
                "security": [
                    {
                        "ApiKey": []
                    },
                    []
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application\/json": {
                            "schema": {
                                "$ref": "#\/components\/schemas\/Address"
                            },
                            "example": {
                                "street": "Musterstraße",
                                "houseNumber": "1a",
                                "zip": "12345",
                                "city": "Musterstadt",
                                "country": "DE"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Abfuhrtermine erfolgreich abgerufen.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/FetchResult"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Ungültige Anfrage oder kein Provider für die Adresse gefunden.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ErrorResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Fehlender oder ungültiger API-Schlüssel."
                    },
                    "404": {
                        "description": "Provider gefunden, aber keine Abfuhrtermine für die Adresse vorhanden.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ErrorResponse"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate-Limit überschritten."
                    }
                }
            }
        },
        "\/api\/fetch-by-position": {
            "post": {
                "summary": "Abfuhrtermine per Position abrufen",
                "description": "Wie POST \/api\/fetch, ermittelt die Adresse aber per Reverse-Geocoding (OpenStreetMap Nominatim) aus einer geografischen Position. Für diesen Endpunkt ist ein gültiger API-Schlüssel zwingend erforderlich.",
                "operationId": "fetchGarbageCollectionDatesByPosition",
                "tags": [
                    "Abfuhrtermine"
                ],
                "security": [
                    {
                        "ApiKey": []
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application\/json": {
                            "schema": {
                                "$ref": "#\/components\/schemas\/Position"
                            },
                            "example": {
                                "latitude": 52.520008,
                                "longitude": 13.404954
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Abfuhrtermine erfolgreich abgerufen. Die aufgelöste Adresse ist im Feld \u0022address\u0022 der Antwort enthalten.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/FetchResult"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Ungültige Anfrage oder keine Position übergeben.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ErrorResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Fehlender oder ungültiger API-Schlüssel."
                    },
                    "404": {
                        "description": "Keine Adresse für die Position ermittelbar oder kein Provider mit Ergebnis.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ErrorResponse"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Position ungültig oder ermittelte Adresse unvollständig.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ErrorResponse"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate-Limit überschritten."
                    },
                    "502": {
                        "description": "Reverse-Geocoding-Dienst nicht erreichbar.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ErrorResponse"
                                }
                            }
                        }
                    }
                }
            }
        },
        "\/api\/health": {
            "get": {
                "summary": "API-Status prüfen",
                "description": "Gibt Statusinformationen zur API zurück: Anzahl der Provider, Provider-Liste und Statistiken der letzten 7 Tage.",
                "operationId": "getApiHealth",
                "tags": [
                    "Status"
                ],
                "security": [],
                "responses": {
                    "200": {
                        "description": "API ist erreichbar.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/HealthResult"
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "summary": "API-Status prüfen (POST)",
                "description": "Identisch mit GET \/api\/health.",
                "operationId": "postApiHealth",
                "tags": [
                    "Status"
                ],
                "security": [],
                "responses": {
                    "200": {
                        "description": "API ist erreichbar.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/HealthResult"
                                }
                            }
                        }
                    }
                }
            }
        },
        "\/api\/webhook": {
            "post": {
                "summary": "Webhook anlegen",
                "description": "Legt einen neuen Webhook an. binable sendet künftig einen HTTP-POST an die angegebene URL, wenn eine Abfuhr in `daysBeforeCollection` Tagen bevorsteht. Das zurückgegebene `secret` dient zur HMAC-SHA256-Signaturprüfung des `X-Binable-Signature`-Headers.",
                "operationId": "createWebhook",
                "tags": [
                    "Webhooks"
                ],
                "security": [
                    {
                        "ApiKey": []
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application\/json": {
                            "schema": {
                                "$ref": "#\/components\/schemas\/WebhookCreateRequest"
                            },
                            "example": {
                                "url": "https:\/\/mein-server.example\/webhook",
                                "daysBeforeCollection": 1,
                                "street": "Musterstraße",
                                "houseNumber": "1a",
                                "zip": "12345",
                                "city": "Musterstadt",
                                "country": "DE"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Webhook erfolgreich angelegt.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/WebhookResponse"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Ungültige Anfrage (fehlende URL, ungültige Adresse oder ungültige Parameter).",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ErrorResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Fehlender oder ungültiger API-Schlüssel."
                    }
                }
            }
        },
        "\/api\/webhook\/{id}": {
            "delete": {
                "summary": "Webhook löschen",
                "description": "Löscht einen bestehenden Webhook. Der Webhook muss dem authentifizierten API-Schlüssel gehören.",
                "operationId": "deleteWebhook",
                "tags": [
                    "Webhooks"
                ],
                "security": [
                    {
                        "ApiKey": []
                    }
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "format": "uuid"
                        },
                        "example": "018f1e2a-3b4c-7d5e-8f9a-0b1c2d3e4f56"
                    }
                ],
                "responses": {
                    "204": {
                        "description": "Webhook erfolgreich gelöscht."
                    },
                    "400": {
                        "description": "Ungültige UUID."
                    },
                    "401": {
                        "description": "Fehlender oder ungültiger API-Schlüssel."
                    },
                    "403": {
                        "description": "Der Webhook gehört einem anderen API-Schlüssel."
                    },
                    "404": {
                        "description": "Webhook nicht gefunden."
                    }
                }
            }
        },
        "\/api\/me": {
            "get": {
                "summary": "API-Schlüssel prüfen",
                "description": "Prüft den mitgesendeten API-Schlüssel. Antwortet mit 200 und den Metadaten des Schlüssels, wenn er gültig ist, und mit 401, wenn er fehlt, unbestätigt oder unbekannt ist.",
                "operationId": "getApiKeyInfo",
                "tags": [
                    "Authentifizierung"
                ],
                "security": [
                    {
                        "ApiKey": []
                    }
                ],
                "responses": {
                    "200": {
                        "description": "API-Schlüssel ist gültig.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ApiKeyInfo"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Fehlender oder ungültiger API-Schlüssel.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ErrorResponse"
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "summary": "API-Schlüssel prüfen (POST)",
                "description": "Identisch mit GET \/api\/me.",
                "operationId": "postApiKeyInfo",
                "tags": [
                    "Authentifizierung"
                ],
                "security": [
                    {
                        "ApiKey": []
                    }
                ],
                "responses": {
                    "200": {
                        "description": "API-Schlüssel ist gültig.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ApiKeyInfo"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Fehlender oder ungültiger API-Schlüssel.",
                        "content": {
                            "application\/json": {
                                "schema": {
                                    "$ref": "#\/components\/schemas\/ErrorResponse"
                                }
                            }
                        }
                    }
                }
            }
        },
        "\/api\/revoke": {
            "post": {
                "summary": "API-Schlüssel widerrufen",
                "description": "Löscht den aktuell authentifizierten API-Schlüssel dauerhaft aus der Datenbank.",
                "operationId": "revokeApiKey",
                "tags": [
                    "Authentifizierung"
                ],
                "security": [
                    {
                        "ApiKey": []
                    }
                ],
                "responses": {
                    "200": {
                        "description": "API-Schlüssel erfolgreich widerrufen."
                    },
                    "401": {
                        "description": "Fehlender oder ungültiger API-Schlüssel."
                    }
                }
            }
        }
    },
    "tags": [
        {
            "name": "Abfuhrtermine",
            "description": "Abfrage von Müllabfuhrterminen nach Adresse"
        },
        {
            "name": "Status",
            "description": "Verfügbarkeit und Statistiken der API"
        },
        {
            "name": "Authentifizierung",
            "description": "Verwaltung des API-Zugangs"
        },
        {
            "name": "Webhooks",
            "description": "Anlegen und Löschen von Webhook-Abonnements"
        }
    ]
}