{
    "openapi": "3.0.0",
    "info": {
        "title": "C4R Aggregator API",
        "description": "Aggregator for public [12-Step Meeting List](https://wordpress.org/plugins/12-step-meeting-list/)\nWordPress feeds. Ingests registered sources on a schedule, normalizes duplicates across feeds,\nand exposes a unified read API whose responses follow the\n[Meeting Guide API spec](https://github.com/code4recovery/spec) for drop-in client compatibility.\n\n- **Public endpoints** return Meeting Guide-shape data. `/meetings` requires at least one filter\n  (bounding box, radius, region, source, program, day, or type) and is cursor-paginated.\n- **Per-source and per-region feeds** return bare JSON arrays — safe drop-in replacements for the\n  upstream TSML endpoint.\n- **Admin endpoints** under `/admin/*` require a Sanctum bearer token.",
        "version": "1.0.0"
    },
    "servers": [
        {
            "url": "https://aggregator.pjbuilds.dev",
            "description": "API server"
        }
    ],
    "paths": {
        "/api/v1/admin/sources": {
            "get": {
                "tags": [
                    "Admin"
                ],
                "summary": "Admin source list with operational metadata",
                "description": "Returns every source (including soft-deleted). Filter by `program` to scope to one program; unknown codes return an empty array.",
                "operationId": "adminListSources",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/program"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "All sources (including soft-deleted)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/AdminSource"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Unauthenticated",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/AuthError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            },
            "post": {
                "tags": [
                    "Admin"
                ],
                "summary": "Register a new feed",
                "description": "Synchronously fetches the upstream feed and validates it has at least one meeting with `slug`, `day`, and `time`. If valid, persists the source in `pending` state and returns a 10-row preview. If invalid, returns 422 with the error type.",
                "operationId": "adminCreateSource",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/CreateSourceRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Created + preview",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CreateSourceResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Unauthenticated",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/AuthError"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Feed validation failed or payload invalid",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/StoreSourceValidationError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            }
        },
        "/api/v1/admin/sources/{source}": {
            "delete": {
                "tags": [
                    "Admin"
                ],
                "summary": "Soft-delete a source",
                "description": "Stops future fetches and marks meetings for stale-sweep. Reversible via `/restore` within 30 days.",
                "operationId": "adminDeleteSource",
                "parameters": [
                    {
                        "name": "source",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Soft-deleted",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SourceEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Unauthenticated"
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            },
            "patch": {
                "tags": [
                    "Admin"
                ],
                "summary": "Update source metadata",
                "operationId": "adminUpdateSource",
                "parameters": [
                    {
                        "name": "source",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/UpdateSourceRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Updated",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SourceEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Unauthenticated"
                    },
                    "404": {
                        "description": "Not found"
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            }
        },
        "/api/v1/admin/sources/{source}/restore": {
            "post": {
                "tags": [
                    "Admin"
                ],
                "summary": "Restore a soft-deleted source within the 30-day window",
                "operationId": "adminRestoreSource",
                "parameters": [
                    {
                        "name": "source",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Restored",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SourceEnvelope"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Restore window expired"
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            }
        },
        "/api/v1/admin/sources/{source}/pause": {
            "post": {
                "tags": [
                    "Admin"
                ],
                "summary": "Pause ingestion (manual operator action)",
                "description": "Meetings remain visible; fetches stop. Use for known temporary upstream outages.",
                "operationId": "adminPauseSource",
                "parameters": [
                    {
                        "name": "source",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Paused",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SourceEnvelope"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            }
        },
        "/api/v1/admin/sources/{source}/resume": {
            "post": {
                "tags": [
                    "Admin"
                ],
                "summary": "Resume ingestion from paused or dormant",
                "operationId": "adminResumeSource",
                "parameters": [
                    {
                        "name": "source",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Resumed",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SourceEnvelope"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            }
        },
        "/api/v1/admin/sources/{source}/refresh": {
            "post": {
                "tags": [
                    "Admin"
                ],
                "summary": "Force-dispatch an immediate ingest job",
                "operationId": "adminRefreshSource",
                "parameters": [
                    {
                        "name": "source",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Queued",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SourceRefreshResponse"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Source is paused or deleted"
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            }
        },
        "/api/v1/admin/sources/{source}/runs": {
            "get": {
                "tags": [
                    "Admin"
                ],
                "summary": "Recent ingest runs for a source",
                "operationId": "adminListSourceRuns",
                "parameters": [
                    {
                        "name": "source",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    },
                    {
                        "name": "limit",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "integer",
                            "default": 25,
                            "maximum": 200,
                            "minimum": 1
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Runs (newest first)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/IngestRun"
                                    }
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            }
        },
        "/api/v1/admin/sources/{source}/dedup-stats": {
            "get": {
                "tags": [
                    "Admin"
                ],
                "summary": "Deduplication counters for a source",
                "description": "Per-layer dedup counters (locations / groups / meetings / types). Returns aggregate totals across every ingest run for this source, plus the counters from the most recent run and the last 10 runs.",
                "operationId": "adminGetSourceDedupStats",
                "parameters": [
                    {
                        "name": "source",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Counters",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DedupStats"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Unauthenticated"
                    },
                    "404": {
                        "description": "Source not found"
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            }
        },
        "/api/v1/map": {
            "get": {
                "tags": [
                    "Catalog"
                ],
                "summary": "All geocoded locations with active meetings, as GeoJSON",
                "description": "One Feature per Location (not per meeting), with the list of meetings-at-that-location in the properties. Intended for a coverage-map view — show pins, click to see meeting names. Cached for 5 minutes (per program filter).",
                "operationId": "getMap",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/program"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "GeoJSON FeatureCollection",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/MapFeatureCollection"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/v1/meetings": {
            "get": {
                "tags": [
                    "Meetings"
                ],
                "summary": "Filtered meeting search",
                "description": "Returns meetings matching the provided filter(s). At least one of `bbox`, `near`, `region_id`, `source_id`, `program`, `day`, or `type` is required. Results are cursor-paginated (default 500/page, max 2000).",
                "operationId": "listMeetings",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/bbox"
                    },
                    {
                        "$ref": "#/components/parameters/near"
                    },
                    {
                        "$ref": "#/components/parameters/radius"
                    },
                    {
                        "$ref": "#/components/parameters/regionId"
                    },
                    {
                        "$ref": "#/components/parameters/sourceId"
                    },
                    {
                        "$ref": "#/components/parameters/program"
                    },
                    {
                        "$ref": "#/components/parameters/day"
                    },
                    {
                        "$ref": "#/components/parameters/type"
                    },
                    {
                        "$ref": "#/components/parameters/includeInactive"
                    },
                    {
                        "$ref": "#/components/parameters/cursor"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Matching meetings (cursor-paginated)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/MeetingPage"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "No filter supplied",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/MeetingsFilterError"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation error",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ValidationError"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/v1/meetings/export": {
            "get": {
                "tags": [
                    "Meetings"
                ],
                "summary": "Streamed NDJSON export of all meetings",
                "description": "Returns every meeting as newline-delimited JSON. Intended for researchers and downstream aggregators. Heavily rate-limited (5/minute) and requires a Sanctum bearer token. Filter by `program` to scope to one program (unknown codes stream an empty body).",
                "operationId": "exportMeetings",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/includeInactive"
                    },
                    {
                        "$ref": "#/components/parameters/program"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Streamed response with `Content-Type: application/x-ndjson`. Body is one serialized `Meeting` JSON object per line separated by `\\n`. No content-schema is emitted because the payload is not a JSON document — consume as a byte stream and split on newlines."
                    },
                    "401": {
                        "description": "Missing or invalid token",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/AuthError"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limit exceeded"
                    }
                },
                "security": [
                    {
                        "bearerAuth": []
                    }
                ]
            }
        },
        "/api/v1/programs": {
            "get": {
                "tags": [
                    "Catalog"
                ],
                "summary": "List supported programs",
                "operationId": "listPrograms",
                "responses": {
                    "200": {
                        "description": "Programs",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Program"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/v1/regions/{region}/feed": {
            "get": {
                "tags": [
                    "Meetings"
                ],
                "summary": "Full Meeting Guide feed for a region and its descendants",
                "description": "Returns a bare JSON array of meetings located within the specified region or any sub-region.",
                "operationId": "getRegionFeed",
                "parameters": [
                    {
                        "name": "region",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Meetings in the region subtree",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Meeting"
                                    }
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Region not found"
                    }
                }
            }
        },
        "/api/v1/sources/{source}/feed": {
            "get": {
                "tags": [
                    "Meetings"
                ],
                "summary": "Full Meeting Guide feed for a single source",
                "description": "Drop-in replacement for the upstream `/wp-json/tsml/meetings` endpoint. Returns a bare JSON array of meetings — no pagination envelope.",
                "operationId": "getSourceFeed",
                "parameters": [
                    {
                        "name": "source",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "All meetings for this source",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Meeting"
                                    }
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Source not found"
                    }
                }
            }
        },
        "/api/v1/sources": {
            "get": {
                "tags": [
                    "Sources"
                ],
                "summary": "Public list of registered sources",
                "description": "Returns every registered source minus admin-only fields (sharing keys, next_fetch_at, failures, etc.). Filter by `program` to scope to one program (e.g. `AA`, `NA`); unknown codes return an empty array.",
                "operationId": "listSources",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/program"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Registered sources",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/PublicSource"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/v1/stats": {
            "get": {
                "tags": [
                    "Catalog"
                ],
                "summary": "Aggregator-wide counts and health snapshot",
                "description": "Cached for 60s in Redis (per program filter). Useful for dashboards, transparency pages, and monitoring. Pass `?program=AA` to scope every count to a single program; `programs_breakdown` is always included so you can see per-program slices alongside the totals.",
                "operationId": "getStats",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/program"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Stats snapshot",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Stats"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/v1/types": {
            "get": {
                "tags": [
                    "Catalog"
                ],
                "summary": "Meeting-type dictionary",
                "description": "Returns all known meeting-type codes. Filter by `program` to scope to one program (e.g. AA).",
                "operationId": "listMeetingTypes",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/program"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Meeting types",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/MeetingType"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "Program": {
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "code": {
                        "type": "string",
                        "example": "AA"
                    },
                    "name": {
                        "type": "string",
                        "example": "Alcoholics Anonymous"
                    }
                },
                "type": "object"
            },
            "MeetingType": {
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "program_id": {
                        "type": "integer"
                    },
                    "code": {
                        "type": "string",
                        "example": "O"
                    },
                    "name": {
                        "type": "string",
                        "example": "Open"
                    },
                    "auto_discovered": {
                        "description": "True if the type was inserted on ingest because no curated seed matched.",
                        "type": "boolean"
                    }
                },
                "type": "object"
            },
            "DedupCounters": {
                "description": "Per-layer dedup counters from a single ingest run or summed across multiple runs.",
                "properties": {
                    "locations_created": {
                        "type": "integer"
                    },
                    "locations_matched_by_upstream_id": {
                        "type": "integer"
                    },
                    "locations_matched_by_address": {
                        "type": "integer"
                    },
                    "locations_matched_by_proximity": {
                        "type": "integer"
                    },
                    "groups_created": {
                        "type": "integer"
                    },
                    "groups_matched_by_upstream_id": {
                        "type": "integer"
                    },
                    "groups_matched_by_name": {
                        "type": "integer"
                    },
                    "meetings_created": {
                        "type": "integer"
                    },
                    "meetings_matched": {
                        "type": "integer"
                    },
                    "types_auto_discovered": {
                        "type": "integer"
                    }
                },
                "type": "object"
            },
            "DedupStatsLatestRun": {
                "description": "Most recent ingest run for a source, with its per-run dedup counters.",
                "properties": {
                    "started_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "finished_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "success",
                            "failed",
                            "suspicious"
                        ]
                    },
                    "counters": {
                        "$ref": "#/components/schemas/DedupCounters"
                    }
                },
                "type": "object",
                "nullable": true
            },
            "DedupStatsRecentRunsInner": {
                "description": "Condensed entry in DedupStats.recent_runs[].",
                "properties": {
                    "started_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "status": {
                        "type": "string"
                    },
                    "counters": {
                        "$ref": "#/components/schemas/DedupCounters"
                    }
                },
                "type": "object"
            },
            "DedupStats": {
                "description": "Dedup activity for a source — aggregate totals, latest run, and recent-run history.",
                "properties": {
                    "source_id": {
                        "type": "integer"
                    },
                    "source_name": {
                        "type": "string"
                    },
                    "run_count": {
                        "type": "integer"
                    },
                    "totals": {
                        "$ref": "#/components/schemas/DedupCounters"
                    },
                    "latest_run": {
                        "$ref": "#/components/schemas/DedupStatsLatestRun"
                    },
                    "recent_runs": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/DedupStatsRecentRunsInner"
                        }
                    }
                },
                "type": "object"
            },
            "ValidationError": {
                "properties": {
                    "message": {
                        "type": "string"
                    },
                    "errors": {
                        "type": "object",
                        "additionalProperties": {
                            "type": "array",
                            "items": {
                                "type": "string"
                            }
                        }
                    }
                },
                "type": "object"
            },
            "MeetingsFilterError": {
                "description": "Returned by `GET /meetings` when no filter is supplied",
                "properties": {
                    "error": {
                        "type": "string",
                        "example": "invalid_filters"
                    },
                    "message": {
                        "type": "string"
                    },
                    "available_filters": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    }
                },
                "type": "object"
            },
            "FeedValidationError": {
                "description": "Returned by `POST /admin/sources` when the upstream feed cannot be validated",
                "properties": {
                    "error": {
                        "type": "string",
                        "example": "invalid_feed"
                    },
                    "error_type": {
                        "type": "string",
                        "enum": [
                            "network",
                            "http",
                            "parse",
                            "empty"
                        ]
                    },
                    "message": {
                        "type": "string"
                    }
                },
                "type": "object"
            },
            "AuthError": {
                "properties": {
                    "message": {
                        "type": "string",
                        "example": "Unauthenticated."
                    }
                },
                "type": "object"
            },
            "MapMeeting": {
                "description": "Minimal meeting shape embedded in a MapFeature.properties.meetings array.",
                "properties": {
                    "name": {
                        "type": "string",
                        "nullable": true
                    },
                    "day": {
                        "type": "integer",
                        "nullable": true,
                        "maximum": 6,
                        "minimum": 0
                    },
                    "time": {
                        "type": "string",
                        "example": "19:30",
                        "nullable": true
                    },
                    "program": {
                        "type": "string",
                        "example": "AA"
                    }
                },
                "type": "object"
            },
            "MapFeatureGeometry": {
                "description": "GeoJSON point geometry for a MapFeature.",
                "properties": {
                    "type": {
                        "type": "string",
                        "example": "Point"
                    },
                    "coordinates": {
                        "description": "[longitude, latitude] — GeoJSON order.",
                        "type": "array",
                        "items": {
                            "type": "number",
                            "format": "float"
                        },
                        "maxItems": 2,
                        "minItems": 2
                    }
                },
                "type": "object"
            },
            "MapFeatureProperties": {
                "description": "Per-location properties on a MapFeature — name/city/state plus the list of active meetings there.",
                "properties": {
                    "id": {
                        "description": "Location id.",
                        "type": "integer"
                    },
                    "name": {
                        "type": "string",
                        "nullable": true
                    },
                    "city": {
                        "type": "string",
                        "nullable": true
                    },
                    "state": {
                        "type": "string",
                        "nullable": true
                    },
                    "meeting_count": {
                        "type": "integer"
                    },
                    "programs": {
                        "description": "Sorted unique program codes hosted at this location — clients can use this to overlay/toggle per-program views without scanning the meetings list.",
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "example": [
                            "AA",
                            "NA"
                        ]
                    },
                    "meetings": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/MapMeeting"
                        }
                    }
                },
                "type": "object"
            },
            "MapFeature": {
                "description": "GeoJSON Feature: one per geocoded location with the list of active meetings at that location.",
                "properties": {
                    "type": {
                        "type": "string",
                        "example": "Feature"
                    },
                    "geometry": {
                        "$ref": "#/components/schemas/MapFeatureGeometry"
                    },
                    "properties": {
                        "$ref": "#/components/schemas/MapFeatureProperties"
                    }
                },
                "type": "object"
            },
            "MapFeatureCollection": {
                "description": "GeoJSON FeatureCollection of all geocoded locations with at least one active meeting. Cached 5 minutes.",
                "properties": {
                    "type": {
                        "type": "string",
                        "example": "FeatureCollection"
                    },
                    "generated_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "features": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/MapFeature"
                        }
                    }
                },
                "type": "object"
            },
            "Meeting": {
                "description": "Meeting Guide API-compliant meeting row. Location and group fields are denormalized for drop-in client compatibility. Adds non-spec `data_freshness_days`.",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "name": {
                        "type": "string",
                        "example": "Pre-Nooners"
                    },
                    "slug": {
                        "type": "string",
                        "example": "pre-nooners"
                    },
                    "day": {
                        "type": "integer",
                        "nullable": true,
                        "maximum": 6,
                        "minimum": 0
                    },
                    "time": {
                        "type": "string",
                        "format": "time",
                        "example": "07:30:00",
                        "nullable": true
                    },
                    "end_time": {
                        "type": "string",
                        "format": "time",
                        "nullable": true
                    },
                    "time_formatted": {
                        "type": "string",
                        "example": "7:30 am",
                        "nullable": true
                    },
                    "updated": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "types": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "example": [
                            "O",
                            "ONL"
                        ]
                    },
                    "notes": {
                        "type": "string",
                        "nullable": true
                    },
                    "approximate": {
                        "type": "string",
                        "enum": [
                            "yes",
                            "no"
                        ]
                    },
                    "attendance_option": {
                        "type": "string",
                        "enum": [
                            "in_person",
                            "online",
                            "hybrid",
                            "inactive"
                        ]
                    },
                    "conference_url": {
                        "type": "string",
                        "format": "uri",
                        "nullable": true
                    },
                    "conference_url_notes": {
                        "type": "string",
                        "nullable": true
                    },
                    "conference_phone": {
                        "type": "string",
                        "nullable": true
                    },
                    "conference_phone_notes": {
                        "type": "string",
                        "nullable": true
                    },
                    "location": {
                        "type": "string",
                        "nullable": true
                    },
                    "location_notes": {
                        "type": "string",
                        "nullable": true
                    },
                    "formatted_address": {
                        "type": "string",
                        "nullable": true
                    },
                    "address": {
                        "type": "string",
                        "nullable": true
                    },
                    "city": {
                        "type": "string",
                        "nullable": true
                    },
                    "state": {
                        "type": "string",
                        "nullable": true
                    },
                    "postal_code": {
                        "type": "string",
                        "nullable": true
                    },
                    "country": {
                        "type": "string",
                        "nullable": true
                    },
                    "latitude": {
                        "type": "number",
                        "format": "float",
                        "nullable": true
                    },
                    "longitude": {
                        "type": "number",
                        "format": "float",
                        "nullable": true
                    },
                    "timezone": {
                        "type": "string",
                        "example": "America/New_York",
                        "nullable": true
                    },
                    "region": {
                        "type": "string",
                        "nullable": true
                    },
                    "group": {
                        "type": "string",
                        "nullable": true
                    },
                    "district": {
                        "type": "string",
                        "nullable": true
                    },
                    "sub_district": {
                        "type": "string",
                        "nullable": true
                    },
                    "website": {
                        "type": "string",
                        "nullable": true
                    },
                    "email": {
                        "type": "string",
                        "nullable": true
                    },
                    "phone": {
                        "type": "string",
                        "nullable": true
                    },
                    "venmo": {
                        "type": "string",
                        "nullable": true
                    },
                    "paypal": {
                        "type": "string",
                        "nullable": true
                    },
                    "square": {
                        "type": "string",
                        "nullable": true
                    },
                    "data_freshness_days": {
                        "description": "Days since the meeting was last seen from an `active`-state source. Null if never seen from active.",
                        "type": "integer",
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "MeetingPageLinks": {
                "description": "Pagination cursor links for MeetingPage.",
                "properties": {
                    "first": {
                        "type": "string",
                        "nullable": true
                    },
                    "last": {
                        "type": "string",
                        "nullable": true
                    },
                    "prev": {
                        "type": "string",
                        "nullable": true
                    },
                    "next": {
                        "type": "string",
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "MeetingPageMeta": {
                "description": "Pagination metadata for MeetingPage.",
                "properties": {
                    "path": {
                        "type": "string"
                    },
                    "per_page": {
                        "type": "integer"
                    },
                    "next_cursor": {
                        "type": "string",
                        "nullable": true
                    },
                    "prev_cursor": {
                        "type": "string",
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "MeetingPage": {
                "description": "Cursor-paginated envelope returned by `GET /meetings`",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Meeting"
                        }
                    },
                    "links": {
                        "$ref": "#/components/schemas/MeetingPageLinks"
                    },
                    "meta": {
                        "$ref": "#/components/schemas/MeetingPageMeta"
                    }
                },
                "type": "object"
            },
            "PublicSource": {
                "description": "Public view of a registered feed. Sharing keys and admin internals are hidden.",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "name": {
                        "type": "string"
                    },
                    "program": {
                        "type": "string",
                        "example": "AA"
                    },
                    "feed_url": {
                        "type": "string",
                        "format": "uri"
                    },
                    "feed_kind": {
                        "type": "string",
                        "enum": [
                            "tsml_json",
                            "google_sheet"
                        ]
                    },
                    "entity": {
                        "type": "string",
                        "nullable": true
                    },
                    "entity_url": {
                        "type": "string",
                        "format": "uri",
                        "nullable": true
                    },
                    "entity_location": {
                        "type": "string",
                        "nullable": true
                    },
                    "state": {
                        "type": "string",
                        "enum": [
                            "pending",
                            "active",
                            "degraded",
                            "failing",
                            "dormant",
                            "paused",
                            "deleted"
                        ]
                    },
                    "last_success_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "meeting_count": {
                        "type": "integer",
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "AdminSource": {
                "description": "Full admin view of a source, including failure state, backoff, and all entity metadata.",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "program": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "feed_url": {
                        "type": "string",
                        "format": "uri"
                    },
                    "feed_kind": {
                        "type": "string",
                        "enum": [
                            "tsml_json",
                            "google_sheet"
                        ]
                    },
                    "state": {
                        "type": "string",
                        "enum": [
                            "pending",
                            "active",
                            "degraded",
                            "failing",
                            "dormant",
                            "paused",
                            "deleted"
                        ]
                    },
                    "fetch_interval_minutes": {
                        "type": "integer"
                    },
                    "last_fetched_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "last_success_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "last_error": {
                        "type": "string",
                        "nullable": true
                    },
                    "last_error_type": {
                        "type": "string",
                        "nullable": true,
                        "enum": [
                            "network",
                            "http",
                            "parse",
                            "empty",
                            "suspicious"
                        ]
                    },
                    "consecutive_failures": {
                        "type": "integer"
                    },
                    "next_fetch_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "last_successful_meeting_count": {
                        "type": "integer",
                        "nullable": true
                    },
                    "days_since_last_success": {
                        "type": "integer",
                        "nullable": true
                    },
                    "paused_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "dormant_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "deleted_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "entity": {
                        "type": "string",
                        "nullable": true
                    },
                    "entity_email": {
                        "type": "string",
                        "nullable": true
                    },
                    "entity_url": {
                        "type": "string",
                        "format": "uri",
                        "nullable": true
                    },
                    "notification_webhook_url": {
                        "type": "string",
                        "format": "uri",
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "IngestRun": {
                "description": "A single fetch attempt against a source",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "source_id": {
                        "type": "integer"
                    },
                    "started_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "finished_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "success",
                            "failed",
                            "suspicious"
                        ]
                    },
                    "was_suspicious": {
                        "type": "boolean"
                    },
                    "error_type": {
                        "type": "string",
                        "nullable": true
                    },
                    "error_message": {
                        "type": "string",
                        "nullable": true
                    },
                    "seen": {
                        "type": "integer"
                    },
                    "created": {
                        "type": "integer"
                    },
                    "updated": {
                        "type": "integer"
                    },
                    "removed": {
                        "type": "integer"
                    }
                },
                "type": "object"
            },
            "CreateSourceRequest": {
                "required": [
                    "feed_url",
                    "program"
                ],
                "properties": {
                    "feed_url": {
                        "type": "string",
                        "format": "uri"
                    },
                    "program": {
                        "type": "string",
                        "example": "AA"
                    },
                    "feed_kind": {
                        "description": "`tsml_json` (default) fetches an upstream Meeting Guide JSON feed. `google_sheet` reads a public Google Sheet backing a tsml-ui front-end (the `data-src` on `<div id=\"tsml-ui\">`); the sheet is fetched via the anonymous gviz CSV export. `google_sheet` sources are forced to run no more than once per day.",
                        "type": "string",
                        "default": "tsml_json",
                        "enum": [
                            "tsml_json",
                            "google_sheet"
                        ]
                    },
                    "name": {
                        "type": "string",
                        "nullable": true
                    },
                    "sharing_key": {
                        "description": "Appended as `?key=…` on each fetch",
                        "type": "string",
                        "nullable": true
                    },
                    "fetch_interval_minutes": {
                        "type": "integer",
                        "default": 1440,
                        "maximum": 10080,
                        "minimum": 5
                    },
                    "notification_webhook_url": {
                        "type": "string",
                        "format": "uri",
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "UpdateSourceRequest": {
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "feed_url": {
                        "type": "string",
                        "format": "uri"
                    },
                    "feed_kind": {
                        "type": "string",
                        "enum": [
                            "tsml_json",
                            "google_sheet"
                        ]
                    },
                    "sharing_key": {
                        "type": "string",
                        "nullable": true
                    },
                    "fetch_interval_minutes": {
                        "type": "integer",
                        "maximum": 10080,
                        "minimum": 5
                    },
                    "notification_webhook_url": {
                        "type": "string",
                        "format": "uri",
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "CreateSourceResponse": {
                "properties": {
                    "source": {
                        "$ref": "#/components/schemas/AdminSource"
                    },
                    "total_meetings_found": {
                        "type": "integer"
                    },
                    "sample_meetings": {
                        "description": "Up to 10 raw upstream rows from the validated feed",
                        "type": "array",
                        "items": {
                            "type": "object"
                        }
                    }
                },
                "type": "object"
            },
            "SourceEnvelope": {
                "description": "{source: AdminSource} wrapper returned by patch/delete/restore/pause/resume.",
                "properties": {
                    "source": {
                        "$ref": "#/components/schemas/AdminSource"
                    }
                },
                "type": "object"
            },
            "SourceRefreshResponse": {
                "description": "Result of queueing an immediate re-fetch for a source.",
                "properties": {
                    "source": {
                        "$ref": "#/components/schemas/AdminSource"
                    },
                    "dispatched": {
                        "type": "boolean"
                    }
                },
                "type": "object"
            },
            "StoreSourceValidationError": {
                "description": "Either a feed-validation error (upstream unreachable / wrong shape) or a standard request-validation error.",
                "oneOf": [
                    {
                        "$ref": "#/components/schemas/FeedValidationError"
                    },
                    {
                        "$ref": "#/components/schemas/ValidationError"
                    }
                ]
            },
            "StatsMeetings": {
                "description": "Meeting-count breakdowns in Stats.",
                "properties": {
                    "total": {
                        "type": "integer"
                    },
                    "active": {
                        "type": "integer"
                    },
                    "inactive": {
                        "type": "integer"
                    },
                    "by_program": {
                        "type": "object",
                        "example": {
                            "AA": 54321
                        },
                        "additionalProperties": {
                            "type": "integer"
                        }
                    },
                    "by_attendance_option": {
                        "type": "object",
                        "example": {
                            "in_person": 30000,
                            "hybrid": 15000,
                            "online": 8001,
                            "inactive": 1320
                        },
                        "additionalProperties": {
                            "type": "integer"
                        }
                    },
                    "by_day": {
                        "description": "Keys are \"0\"–\"6\" (Sun–Sat)",
                        "type": "object",
                        "additionalProperties": {
                            "type": "integer"
                        }
                    }
                },
                "type": "object"
            },
            "StatsLocations": {
                "description": "Location-count breakdowns in Stats.",
                "properties": {
                    "total": {
                        "type": "integer"
                    },
                    "with_coords": {
                        "type": "integer"
                    }
                },
                "type": "object"
            },
            "StatsGroups": {
                "description": "Group-count breakdowns in Stats.",
                "properties": {
                    "total": {
                        "type": "integer"
                    }
                },
                "type": "object"
            },
            "StatsSourcesTopByMeetingCountInner": {
                "description": "Entry in Stats.sources.top_by_meeting_count[].",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "name": {
                        "type": "string"
                    },
                    "meeting_count": {
                        "type": "integer"
                    }
                },
                "type": "object"
            },
            "StatsSources": {
                "description": "Source-count breakdowns in Stats.",
                "properties": {
                    "total": {
                        "type": "integer"
                    },
                    "by_state": {
                        "type": "object",
                        "example": {
                            "active": 90,
                            "degraded": 4,
                            "dormant": 2
                        },
                        "additionalProperties": {
                            "type": "integer"
                        }
                    },
                    "top_by_meeting_count": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/StatsSourcesTopByMeetingCountInner"
                        }
                    }
                },
                "type": "object"
            },
            "StatsMeetingTypes": {
                "description": "Meeting-type-count breakdowns in Stats.",
                "properties": {
                    "total": {
                        "type": "integer"
                    }
                },
                "type": "object"
            },
            "StatsProgramsBreakdownInner": {
                "description": "Per-program counts. When `?program=` filters the response, the breakdown is scoped to that program; otherwise it lists every registered program.",
                "properties": {
                    "code": {
                        "type": "string",
                        "example": "AA"
                    },
                    "name": {
                        "type": "string",
                        "example": "Alcoholics Anonymous"
                    },
                    "meetings_total": {
                        "type": "integer"
                    },
                    "meetings_active": {
                        "type": "integer"
                    },
                    "sources_total": {
                        "type": "integer"
                    },
                    "sources_active": {
                        "type": "integer"
                    }
                },
                "type": "object"
            },
            "Stats": {
                "description": "Aggregator-wide counts snapshot. Cached 60 seconds (per program filter).",
                "properties": {
                    "generated_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "program": {
                        "description": "Echoes the `?program=` filter, or null when unfiltered.",
                        "type": "string",
                        "example": "AA",
                        "nullable": true
                    },
                    "meetings": {
                        "$ref": "#/components/schemas/StatsMeetings"
                    },
                    "locations": {
                        "$ref": "#/components/schemas/StatsLocations"
                    },
                    "groups": {
                        "$ref": "#/components/schemas/StatsGroups"
                    },
                    "sources": {
                        "$ref": "#/components/schemas/StatsSources"
                    },
                    "meeting_types": {
                        "$ref": "#/components/schemas/StatsMeetingTypes"
                    },
                    "programs_breakdown": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/StatsProgramsBreakdownInner"
                        }
                    },
                    "last_successful_ingest_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    }
                },
                "type": "object"
            }
        },
        "parameters": {
            "bbox": {
                "name": "bbox",
                "in": "query",
                "description": "Bounding box filter: `minLat,minLng,maxLat,maxLng`",
                "required": false,
                "schema": {
                    "type": "string",
                    "example": "32.5,-80.5,33.5,-79.5"
                }
            },
            "near": {
                "name": "near",
                "in": "query",
                "description": "Center-point for radius search: `lat,lng`",
                "required": false,
                "schema": {
                    "type": "string",
                    "example": "33.015,-80.236"
                }
            },
            "radius": {
                "name": "radius",
                "in": "query",
                "description": "Radius in miles when `near` is present. Default 25, max 50.",
                "required": false,
                "schema": {
                    "type": "number",
                    "format": "float",
                    "default": 25,
                    "maximum": 50,
                    "minimum": 0.1
                }
            },
            "regionId": {
                "name": "region_id",
                "in": "query",
                "description": "Limit to meetings in this region (or any descendant)",
                "required": false,
                "schema": {
                    "type": "integer"
                }
            },
            "sourceId": {
                "name": "source_id",
                "in": "query",
                "description": "Limit to meetings from this source",
                "required": false,
                "schema": {
                    "type": "integer"
                }
            },
            "program": {
                "name": "program",
                "in": "query",
                "description": "Program code (`AA`, `NA`, ...)",
                "required": false,
                "schema": {
                    "type": "string",
                    "example": "AA"
                }
            },
            "day": {
                "name": "day",
                "in": "query",
                "description": "Day of week: 0=Sunday ... 6=Saturday",
                "required": false,
                "schema": {
                    "type": "integer",
                    "maximum": 6,
                    "minimum": 0
                }
            },
            "type": {
                "name": "type",
                "in": "query",
                "description": "Comma-separated meeting-type codes. Meetings must match every code listed.",
                "required": false,
                "schema": {
                    "type": "string",
                    "example": "O,D"
                }
            },
            "includeInactive": {
                "name": "include_inactive",
                "in": "query",
                "description": "Include meetings marked inactive by stale-sweep. Default false.",
                "required": false,
                "schema": {
                    "type": "boolean",
                    "default": false
                }
            },
            "cursor": {
                "name": "cursor",
                "in": "query",
                "description": "Opaque cursor for pagination, returned in `next_cursor` / `prev_cursor`",
                "required": false,
                "schema": {
                    "type": "string"
                }
            },
            "limit": {
                "name": "limit",
                "in": "query",
                "description": "Rows per page (default 500, max 2000)",
                "required": false,
                "schema": {
                    "type": "integer",
                    "default": 500,
                    "maximum": 2000,
                    "minimum": 1
                }
            }
        },
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "description": "Sanctum personal access token. Obtain via `php artisan tsml:token <email>`.",
                "scheme": "bearer"
            }
        }
    },
    "tags": [
        {
            "name": "Meetings",
            "description": "Filter-required meeting queries and full feeds"
        },
        {
            "name": "Sources",
            "description": "Registered TSML feeds (public view)"
        },
        {
            "name": "Catalog",
            "description": "Programs and meeting-type dictionaries"
        },
        {
            "name": "Admin",
            "description": "Source management — requires Sanctum bearer token"
        }
    ]
}