{
  "openapi": "3.1.0",
  "info": {
    "title": "Neighborhood Commons API",
    "description": "The Neighborhood Commons API: typed substrate for neighborhood-scale public facts. Five Schema.org-aligned primitives (Place, Organization, Event, Broadcast, List), verification anchored on Type A profile authority, constrained-publishing model with three authority paths (entity-runs-it, pipeline-proxies, witnessed-with-evidence). Read free, write through linked organizations. CC BY 4.0.\n\n**This document is the Spec, part of the Commons Contract.** The Contract is three files together: the Spec (this document — machine-readable, authoritative), the Guide (`/llms.txt` — narrative companion), and the Log (`CHANGELOG.md` — dated record of every change). Rule when they disagree: Spec wins. Guide explains. Log dates.\n\n**3.0.0** establishes the standard four-role event provenance frame (`docs/four-roles.md`) and the type-general provenance method doctrine (`docs/provenance.md`). `source.publisher` is removed (the role is filled by `organizer.name`); `source.method` uses the standard vocabulary (`self_asserted` / `proxied` / `witnessed`); `source.url` carries the proxied source URL when applicable. Organizations, Broadcasts, and Lists gain a `method` field — Organizations admit `seeded` for bulk-imported, awaiting-uptake rows. This is the launch contract; from here forward the substrate is additive-only.\n\n**3.1.0** (additive) adds Contributor Profiles — the public-facing identity of each contributing app. New public reads at `/v1/contributors` and `/v1/contributors/{idOrSlug}` surface the slug, name, tagline, description, logo, and app URL. Event responses gain `source.contributor.{slug, logo_url, description, profile_url}` when the event is linked to a registered contributor profile; pre-3.1 events fall back to the existing `name`/`url` snapshot fields. Foundation for the self-service developer dashboard at `/developers` (PRs 2–5).\n\n**3.2.0** (additive) makes the Series primitive addressable: `/v1/series` and `/v1/series/{idOrSlug}` expose recurring activity with its own identity (name, slug, description, cover image, organizer, recurrence, next instance); `PATCH /service/series/{id}` edits identity and `POST /service/series/{id}/cover` uploads the cover. Adds the `created_by_contributor` filter to events, organizations, and publishers — slice by the app that published a record. Opens caller-set `proxied` provenance: keys with `proxy_authority` publish scrape-and-publish events carrying a writable `source_feed_url`, making four-role Path 2 reachable for external pipelines.",
    "version": "3.2.0",
    "license": {
      "name": "CC-BY-4.0",
      "url": "https://creativecommons.org/licenses/by/4.0/"
    },
    "contact": {
      "name": "Neighborhood Commons",
      "url": "https://neighborhood-commons.org",
      "email": "hi@neighborhood-commons.org"
    }
  },
  "servers": [
    {
      "url": "https://neighborhood-commons.org/api/v1",
      "description": "Production"
    }
  ],
  "paths": {
    "/events": {
      "get": {
        "summary": "List events",
        "description": "Published events, paginated and filtered. No authentication required.",
        "operationId": "listEvents",
        "parameters": [
          {
            "name": "start_after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Events starting after this date (YYYY-MM-DD)"
          },
          {
            "name": "start_before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Events starting before this date (YYYY-MM-DD)"
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by category slug"
          },
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ]
            },
            "description": "Filter by tag(s) — AND semantics"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Text search across name and description"
          },
          {
            "name": "near",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^-?\\d+\\.?\\d*,-?\\d+\\.?\\d*$"
            },
            "description": "Coordinates as lat,lng for proximity filtering"
          },
          {
            "name": "radius_km",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": 0.1,
              "maximum": 100,
              "default": 10
            },
            "description": "Proximity radius in km (requires near)"
          },
          {
            "name": "collapse_series",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "Deduplicate series to nearest upcoming instance"
          },
          {
            "name": "series_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Filter to events in a specific series"
          },
          {
            "name": "recurring",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "Filter recurring (true) or one-off (false) events"
          },
          {
            "name": "contributor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by organizer organization slug. Resolves against `organizations.slug` → `events.organizer_org_id`."
          },
          {
            "name": "created_by_contributor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter to events contributed by this app — the publishing-app axis (`source.contributor`). Resolves against the registered `contributor_profile` slug; only `active` profiles match. Distinct from `contributor`, which filters by the organizer organization."
          },
          {
            "name": "commercial",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["true", "false"]
            },
            "description": "Filter to events whose organizer is for-profit (`commercial=true`) vs. non-commercial (`commercial=false`). Joins through `organizations.commercial`."
          },
          {
            "name": "place_category",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 50
            },
            "description": "Filter to events at places with the given OSM-sourced category (e.g., `cafe`, `live_music_venue`). Joins through `places.place_categories`."
          },
          {
            "name": "tmdb_id",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 50
            },
            "description": "Filter to all showings of a film (clusters film-category events across theaters and dates). Pair with `?category=film` for clean results."
          },
          {
            "name": "first_party",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "Filter by authorship tier. `true` = events posted by the verified business itself (first-party). `false` = events aggregated from public sources (scrapers, feeds, ingestion pipelines). Omit for both. The Commons separates information *from* a business from information *about* a business; apps choose what tier to surface."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            },
            "description": "Page size"
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Page offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated event list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "meta",
                    "events"
                  ],
                  "properties": {
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Event"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/events/changes": {
      "servers": [
        {
          "url": "https://neighborhood-commons.org/api",
          "description": "Changes feed lives OUTSIDE the /v1 prefix. Full URL: https://neighborhood-commons.org/api/events/changes"
        }
      ],
      "get": {
        "summary": "Events changes feed",
        "description": "Lightweight public sync endpoint for consumers without a service key. Returns events whose updated_at is greater than the `since` cursor, plus the IDs of events that have ended since that cursor. Rate limit: 10/min per IP. Responses are cached 60s.\n\n**Path note:** this endpoint is mounted at `/api/events/changes` — outside the `/api/v1` prefix that the rest of the spec uses. The OpenAPI path-level `servers` override above reflects this. **SDK callers (openapi-fetch et al. that don't honor path-level overrides):** either construct a second client with `baseUrl` pointed at `https://neighborhood-commons.org/api`, or call this endpoint with raw `fetch`. The SDK README documents the workaround.\n\nFor higher throughput, use a service key and call `/v1/service/events` instead.",
        "operationId": "getChangesFeed",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO 8601 timestamp. Returns events updated after this point."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Changes since the cursor",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "events",
                    "deleted_ids",
                    "sync_cursor",
                    "has_more"
                  ],
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "content": {
                            "type": "string"
                          },
                          "description": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "place_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "event_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "end_time": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "category": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "link_url": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "event_image_url": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "region_slug": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "updated_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "deleted_ids": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "uuid"
                      }
                    },
                    "sync_cursor": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Pass this as the next request's `since` value."
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "If true, another page of changes exists — call again with the returned sync_cursor."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/events/{id}": {
      "get": {
        "summary": "Get event",
        "description": "Single event by ID in Neighborhood API format.",
        "operationId": "getEvent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Event detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "event": {
                      "$ref": "#/components/schemas/Event"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/events/terms": {
      "get": {
        "summary": "Data license terms",
        "operationId": "getTerms",
        "responses": {
          "200": {
            "description": "License and usage terms",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/events.ics": {
      "get": {
        "summary": "iCalendar feed",
        "description": "iCalendar feed of events. Accepts the same query filters as /events. Default window: 30 days ago to 90 days ahead. Series are deduplicated with RRULE.",
        "operationId": "getIcalFeed",
        "parameters": [
          {
            "name": "contributor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by organizer organization slug. Resolves against `organizations.slug` → `events.organizer_org_id`."
          },
          {
            "name": "created_by_contributor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter to events contributed by this app — the publishing-app axis (`source.contributor`). Resolves against the registered `contributor_profile` slug; only `active` profiles match. Distinct from `contributor`, which filters by the organizer organization."
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by category slug"
          },
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ]
            },
            "description": "Filter by tag(s)"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Text search"
          },
          {
            "name": "near",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coordinates as lat,lng"
          },
          {
            "name": "radius_km",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "Proximity radius in km"
          },
          {
            "name": "start_after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Override default 30-day lookback"
          },
          {
            "name": "start_before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Override default 90-day lookahead"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 500,
              "default": 200
            },
            "description": "Max events"
          }
        ],
        "responses": {
          "200": {
            "description": "iCalendar feed of matching events",
            "content": {
              "text/calendar": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/events.rss": {
      "get": {
        "summary": "RSS feed",
        "description": "RSS 2.0 feed of events. Accepts the same query filters as /events. Supports pagination via limit/offset.",
        "operationId": "getRssFeed",
        "parameters": [
          {
            "name": "contributor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by organizer organization slug. Resolves against `organizations.slug` → `events.organizer_org_id`."
          },
          {
            "name": "created_by_contributor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter to events contributed by this app — the publishing-app axis (`source.contributor`). Resolves against the registered `contributor_profile` slug; only `active` profiles match. Distinct from `contributor`, which filters by the organizer organization."
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by category slug"
          },
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ]
            },
            "description": "Filter by tag(s)"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Text search"
          },
          {
            "name": "near",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Coordinates as lat,lng"
          },
          {
            "name": "radius_km",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "Proximity radius in km"
          },
          {
            "name": "start_after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Events after this date"
          },
          {
            "name": "start_before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Events before this date"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 200,
              "default": 50
            },
            "description": "Page size"
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Page offset"
          }
        ],
        "responses": {
          "200": {
            "description": "RSS 2.0 feed of matching events",
            "content": {
              "application/rss+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/meta": {
      "get": {
        "summary": "Feed metadata",
        "operationId": "getMeta",
        "responses": {
          "200": {
            "description": "Feed name, stewards, license, and supported resources",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/meta/regions": {
      "get": {
        "summary": "List active regions",
        "description": "Geographic regions with timezone and center coordinates. Cached for 1 hour.",
        "operationId": "listRegions",
        "responses": {
          "200": {
            "description": "Active regions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "regions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "timezone": {
                            "type": "string"
                          },
                          "latitude": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "longitude": {
                            "type": [
                              "number",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/meta/categories": {
      "get": {
        "summary": "List event categories with counts",
        "description": "Categories with current event counts. Sorted by count descending. Cached for 30 minutes.",
        "operationId": "listCategories",
        "responses": {
          "200": {
            "description": "Event categories",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string",
                            "description": "Kebab-case slug (e.g. live-music)"
                          },
                          "key": {
                            "type": "string",
                            "description": "Underscore key (e.g. live_music)"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/meta/stats": {
      "get": {
        "summary": "Platform statistics",
        "description": "Live counts of published events, active venues, and primary region. No authentication required. Cached for 5 minutes.",
        "operationId": "getStats",
        "responses": {
          "200": {
            "description": "Platform statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total_events": {
                      "type": "integer",
                      "description": "Published events"
                    },
                    "total_venues": {
                      "type": "integer",
                      "description": "Active venue accounts"
                    },
                    "region": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Primary region name"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/webhooks": {
      "post": {
        "summary": "Create webhook subscription",
        "description": "Subscribe to event changes. Returns the signing secret once — store it securely. Webhooks are signed with HMAC-SHA256. Max 5 subscriptions per API key.\n\n**Event types:** `event.created`, `event.updated`, `event.deleted`, `event.series_created`, `event.image_processed`, `series.updated`, `series.deleted`. The default subscribes to the event.* types except `event.image_processed`. `series.updated` and `series.deleted` fire when a series identity changes or the series is removed — useful for consumers caching series pages.",
        "operationId": "createWebhook",
        "security": [
          {
            "browseApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "HTTPS endpoint. Must resolve to a public IP (RFC 1918 and cloud metadata addresses blocked)."
                  },
                  "event_types": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "event.created",
                        "event.updated",
                        "event.deleted",
                        "event.series_created",
                        "event.image_processed",
                        "series.updated",
                        "series.deleted"
                      ]
                    },
                    "default": [
                      "event.created",
                      "event.updated",
                      "event.deleted",
                      "event.series_created"
                    ],
                    "description": "Event types to subscribe to. Defaults to all event.* types except `event.image_processed`, which has a different payload shape and must be opted into explicitly."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Subscription created. The signing_secret is returned only on creation — it cannot be recovered later.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/Webhook"
                    },
                    "signing_secret": {
                      "type": "string",
                      "description": "64-character hex secret for HMAC-SHA256 verification. Shown once."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid URL or URL validation failed (SSRF protection). Error codes: INVALID_WEBHOOK_URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Subscription limit reached (max 5 per API key)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "summary": "List webhook subscriptions",
        "description": "List all webhook subscriptions for the calling API key.",
        "operationId": "listWebhooks",
        "security": [
          {
            "browseApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of webhook subscriptions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/webhooks/{id}": {
      "patch": {
        "summary": "Update webhook subscription",
        "description": "Update the URL, event types, or status (active/paused) of a webhook subscription.",
        "operationId": "updateWebhook",
        "security": [
          {
            "browseApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "New HTTPS endpoint URL"
                  },
                  "event_types": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "event.created",
                        "event.updated",
                        "event.deleted",
                        "event.series_created",
                        "event.image_processed",
                        "series.updated",
                        "series.deleted"
                      ]
                    }
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "paused"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated subscription",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/Webhook"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Webhook belongs to a different API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "summary": "Delete webhook subscription",
        "description": "Delete a webhook subscription. Future events will not be delivered to this endpoint.",
        "operationId": "deleteWebhook",
        "security": [
          {
            "browseApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Webhook belongs to a different API key"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/webhooks/{id}/test": {
      "post": {
        "summary": "Send test webhook delivery",
        "description": "Send a test payload to the webhook endpoint. Useful for verifying signature verification logic.",
        "operationId": "testWebhook",
        "security": [
          {
            "browseApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Test delivery result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "delivered": {
                      "type": "boolean"
                    },
                    "status_code": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Webhook belongs to a different API key"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/webhooks/{id}/deliveries": {
      "get": {
        "summary": "List webhook deliveries",
        "description": "Recent delivery attempts for a webhook subscription, newest first. Filter by `status` and/or `event_id` to confirm a specific event reached this subscriber — useful for reconciling missed deliveries without a full re-scan.",
        "operationId": "listWebhookDeliveries",
        "security": [
          {
            "browseApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "delivered",
                "failed",
                "retrying"
              ]
            },
            "description": "Filter to a single delivery state."
          },
          {
            "name": "event_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Filter to deliveries for a specific event UUID. Combine with `status=delivered` to confirm a specific event landed."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delivery history",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deliveries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Auto-incrementing delivery row ID."
                          },
                          "event_type": {
                            "type": "string",
                            "description": "The event_type that triggered this delivery (e.g. `event.created`, `event.image_processed`)."
                          },
                          "event_id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The event UUID this delivery is for."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "delivered",
                              "failed",
                              "retrying"
                            ]
                          },
                          "status_code": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "HTTP status code returned by the subscriber endpoint, or null if no response was received."
                          },
                          "error_message": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Failure reason (truncated to 500 chars). Null on success."
                          },
                          "attempt": {
                            "type": "integer",
                            "description": "Attempt number, 1-indexed. Max 3."
                          },
                          "next_retry_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "When the next retry will fire. Null on terminal states."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total": { "type": "integer" },
                        "limit": { "type": "integer" },
                        "offset": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Webhook belongs to a different API key"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/service/events": {
      "get": {
        "summary": "List events (service)",
        "description": "List events with full internal fields. Supports filtering by status, time, category, search, and source method. By default shows one-offs + first instance of each series; pass all_instances=true for reconciliation.",
        "operationId": "serviceListEvents",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "published",
                "pending_review",
                "draft"
              ]
            }
          },
          {
            "name": "time",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "upcoming",
                "past",
                "all"
              ]
            },
            "description": "Default: upcoming"
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Text search on title, venue name, address"
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source_method",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "self_asserted",
                "proxied",
                "witnessed"
              ]
            }
          },
          {
            "name": "all_instances",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "Include all series instances (default: false)"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 500
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Events with account info",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": {
                      "type": "array"
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Create event (service)",
        "description": "Create a single or recurring event on behalf of a linked account. Uses Neighborhood API friendly-shape field names (name, start, timezone, location, url, cost) — symmetric with GET /events responses. Omit recurrence for one-off events. For recurring events, creates a full series and returns series_count and instance_ids.",
        "operationId": "serviceCreateEvent",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceEventInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Event or series created",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "description": "Single event",
                      "properties": {
                        "event": {
                          "type": "object"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "description": "Recurring series",
                      "properties": {
                        "series_count": {
                          "type": "integer"
                        },
                        "series_id": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "uuid"
                        },
                        "instance_ids": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "format": "uuid"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Validation error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "NOT_LINKED — service key not linked to account"
          },
          "404": {
            "description": "Account not found"
          }
        }
      }
    },
    "/service/events/batch": {
      "patch": {
        "summary": "Bulk update events (service)",
        "description": "Bulk-update up to 200 events by ID. Only updates provided fields. Scoped to accounts linked to this key (admin keys bypass).",
        "operationId": "serviceBatchUpdateEvents",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids",
                  "updates"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "minItems": 1,
                    "maxItems": 200
                  },
                  "updates": {
                    "type": "object",
                    "properties": {
                      "category": {
                        "type": "string"
                      },
                      "tags": {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "maxLength": 100
                        },
                        "maxItems": 15
                      },
                      "description": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 2000
                      },
                      "price": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 100
                      },
                      "wheelchair_accessible": {
                        "type": [
                          "boolean",
                          "null"
                        ]
                      },
                      "open_window": {
                        "type": "boolean"
                      },
                      "capacity": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "minimum": 1,
                        "maximum": 10000
                      },
                      "rsvp": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "enum": [
                          "recommended",
                          "required",
                          null
                        ]
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "published",
                          "pending_review",
                          "draft"
                        ]
                      },
                      "first_party": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch update complete",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updated_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "NOT_LINKED for one or more events"
          }
        }
      }
    },
    "/service/events/{id}": {
      "get": {
        "summary": "Get event (service)",
        "description": "Get a single event with full internal fields and account info.",
        "operationId": "serviceGetEvent",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Event with account",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "event"
                  ],
                  "properties": {
                    "event": {
                      "$ref": "#/components/schemas/ServiceEvent"
                    },
                    "account": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/ServiceAccount"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "summary": "Update event (service)",
        "description": "Update a single event. Scoped to accounts linked to this service key.",
        "operationId": "serviceUpdateEvent",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceEventInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Event updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "event"
                  ],
                  "properties": {
                    "event": {
                      "$ref": "#/components/schemas/ServiceEvent"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "NOT_LINKED"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "summary": "Delete event (service)",
        "description": "Delete a single event. Scoped to accounts linked to this service key.",
        "operationId": "serviceDeleteEvent",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "NOT_LINKED"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/events/series/{seriesId}": {
      "patch": {
        "summary": "Update series (service)",
        "description": "Update all future instances of a recurring series. Scoped to accounts linked to this service key.",
        "operationId": "serviceUpdateSeries",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "seriesId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceEventInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Series updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updated_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "NOT_LINKED"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "summary": "Delete series (service)",
        "description": "Delete all future instances of a recurring series. Scoped to accounts linked to this service key.",
        "operationId": "serviceDeleteSeries",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "seriesId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Series deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "NOT_LINKED"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/series/{seriesId}": {
      "patch": {
        "summary": "Update series identity (service)",
        "description": "Update identity fields on a series — name, slug, description, cover image. Distinct from `PATCH /service/events/series/{seriesId}` (which patches the per-instance template and propagates to future instance rows). Identity edits do NOT rewrite past or future instance titles — to rename future instances too, call the template-patch endpoint with the same name. Fires the `series.updated` webhook.\n\nScoped to organizations linked to this service key via `api_key_organization_links`.",
        "operationId": "serviceUpdateSeriesIdentity",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "seriesId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeriesIdentityInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Series identity updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "series_id",
                    "changed"
                  ],
                  "properties": {
                    "series_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "changed": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "name",
                          "slug",
                          "description",
                          "cover_image_url"
                        ]
                      },
                      "description": "Field names that actually changed. Empty array if the request was a no-op (sent values matched existing)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "NOT_LINKED — key not linked to this series's organizer organization."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Slug already in use."
          }
        }
      }
    },
    "/service/series/{seriesId}/cover": {
      "post": {
        "summary": "Upload series cover image (service)",
        "description": "Upload a cover image for a series. Image is magic-byte validated and re-encoded through Sharp (JPEG/PNG/WebP only, max 12MB), then stored on the Commons R2 bucket — the same bucket every other Commons-hosted image uses. Don't process series covers through a per-app pipeline; cross-product coherence depends on every consumer rendering the same URL pattern.\n\nPersists the resulting URL to `event_series.cover_image_url` and fires the `series.updated` webhook with `changed=['cover_image_url']`. Scoped via `api_key_organization_links` against `event_series.organizer_org_id`.",
        "operationId": "serviceUploadSeriesCover",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "seriesId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "description": "Base64-encoded image bytes. Optional `data:image/...;base64,` prefix is stripped. Supported formats: JPEG, PNG, WebP. Max 12MB raw."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cover uploaded; URL persisted on the series row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "series_id",
                    "cover_image_url"
                  ],
                  "properties": {
                    "series_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "cover_image_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "NOT_LINKED — key not linked to this series's organizer organization."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "description": "PAYLOAD_TOO_LARGE — image exceeds 12MB."
          }
        }
      }
    },
    "/service/events/{id}/image": {
      "post": {
        "summary": "Upload event image (service)",
        "description": "Upload an image for an event. Image is re-encoded through Sharp (JPEG/PNG/WebP only, max 12MB). Returns the CDN URL.",
        "operationId": "serviceUploadEventImage",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "description": "Base64-encoded image data"
                  },
                  "content_type": {
                    "type": "string",
                    "enum": [
                      "image/jpeg",
                      "image/png",
                      "image/webp"
                    ]
                  },
                  "focal_y": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1,
                    "description": "Vertical focal point for cropping (0=top, 1=bottom, 0.5=center)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Image uploaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "event_image_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid image type or corrupt file"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "NOT_LINKED"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/stats": {
      "get": {
        "summary": "Platform stats (service/admin)",
        "description": "Detailed stats: account counts (total, claimed, pending), event counts, and category distribution. Admin service key required.",
        "operationId": "serviceGetStats",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Platform stats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stats": {
                      "type": "object",
                      "properties": {
                        "total_accounts": {
                          "type": "integer"
                        },
                        "claimed_accounts": {
                          "type": "integer"
                        },
                        "pending_accounts": {
                          "type": "integer"
                        },
                        "total_events": {
                          "type": "integer"
                        },
                        "category_distribution": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          }
        }
      }
    },
    "/service/api-keys": {
      "get": {
        "summary": "List API keys (service/admin)",
        "description": "List all API keys with event contribution stats. Admin service key required.",
        "operationId": "serviceListApiKeys",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "API keys with event stats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_keys": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "key_prefix": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "url": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "contact_email": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "rate_limit_per_hour": {
                            "type": "integer"
                          },
                          "status": {
                            "type": "string"
                          },
                          "contributor_tier": {
                            "type": "string"
                          },
                          "last_used_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "event_count": {
                            "type": "integer"
                          },
                          "pending_count": {
                            "type": "integer"
                          },
                          "last_submitted_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          }
        }
      },
      "post": {
        "summary": "Issue API key (service/admin)",
        "description": "Create a new API key. Writeable scope (which organizations the key may publish for) is established separately via `POST /service/organizations/link` after issuance. The optional `account_id` field references an operational tenant portal_account but does not grant write authority on its own. Returns the raw key string ONCE — store it immediately, it is unrecoverable.",
        "operationId": "serviceCreateApiKey",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "contact_email"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "contact_email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 200
                  },
                  "contributor_tier": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "verified",
                      "trusted",
                      "service"
                    ],
                    "default": "verified"
                  },
                  "account_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Optional. Operational reference to a portal_account the key represents (tenant claim tracking). Does not grant write authority — that flows through `api_key_organization_links`."
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 500
                  },
                  "rate_limit_per_hour": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000,
                    "default": 1000
                  },
                  "is_admin": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created. The `key` field is the raw secret — store it immediately.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "api_key",
                    "key"
                  ],
                  "properties": {
                    "api_key": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "key_prefix": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "contributor_tier": {
                          "type": "string"
                        },
                        "is_admin": {
                          "type": "boolean"
                        },
                        "account_id": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "uuid"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "key": {
                      "type": "string",
                      "description": "The raw API key — pass as X-API-Key. Returned ONCE; not recoverable."
                    },
                    "warning": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          },
          "404": {
            "description": "account_id not found"
          }
        }
      }
    },
    "/service/api-keys/{id}": {
      "patch": {
        "summary": "Update API key (service/admin)",
        "description": "Update an API key's name, URL, status, contributor_tier, or contact_email. Admin service key required.",
        "operationId": "serviceUpdateApiKey",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uri",
                    "maxLength": 500
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "revoked"
                    ]
                  },
                  "contributor_tier": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "verified",
                      "trusted"
                    ]
                  },
                  "contact_email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "API key updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_key": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No fields to update"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/api-keys/{id}/activate": {
      "post": {
        "summary": "Activate a pending service-tier key (admin)",
        "description": "Flip a self-registered service-tier API key from pending (`activated_at` NULL — reads only) to fully active (`activated_at` set — writes resume). Optionally sets `brand_config`, `verification_authority`, `rate_limit_per_hour`, and `provision_account` in the same call. Admin service key required. Idempotent: returns `already_active: true` if the key was previously activated.\n\n**Atomic tenant provisioning (`provision_account`)**: tenant-umbrella consumers (Merrie, GoThere, etc.) include `provision_account` in their activation request email. The operator passes those values through to this endpoint, which atomically (a) flips the key to active, (b) creates the consumer's tenant portal_account, and (c) links the now-active key to it. The response includes the new `account.id` — the operator forwards it to the consumer alongside the activation confirmation. The consumer never makes a second round-trip for tenant provisioning.\n\nThis is the canonical activation path for tenant-umbrella consumers. Per-operator portable consumers omit `provision_account` and call `POST /service/accounts/link` per operator as those operators come online — both forms remain supported.\n\n**Why atomic?** Pending keys are strictly read-only — no portal_account exists before activation. The `provision_account` body collapses the publisher-identity creation into the single quality-gate moment, so consumers never have a pre-activation footprint and admin's action is one call.",
        "operationId": "serviceActivateApiKey",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "brand_config": {
                    "type": "object",
                    "description": "Per-key sender identity for verification emails. Operator-set at activation."
                  },
                  "verification_authority": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Methods this key may auto-approve, e.g. [\"manual_review:in_person\"]."
                  },
                  "rate_limit_per_hour": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000
                  },
                  "provision_account": {
                    "type": "object",
                    "description": "Optional tenant-account provisioning bundled with activation. Creates a portal_account with the supplied identity and links the now-active key to it. Omit for per-operator portable consumers.",
                    "required": [
                      "email",
                      "business_name"
                    ],
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email",
                        "maxLength": 254,
                        "description": "Sentinel email on a domain the consumer controls (e.g. `tenant@no-reply.consumer-app.com`). Never used for OTP claim; identifies the publisher row."
                      },
                      "business_name": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 200,
                        "description": "Public-facing tenant name. Surfaces as `source.publisher` on events when no other publisher is set."
                      },
                      "claimed_by": {
                        "type": "string",
                        "maxLength": 50,
                        "description": "Short slug recording which app claimed this account. Defaults to `\"api\"`. Future link attempts under a different slug are refused with 409 CONFLICT."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key activated, or already active. If `provision_account` was supplied on a fresh activation, the response also includes `account`, `account_created`, and `account_linked`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_key": {
                      "type": "object"
                    },
                    "api_key_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "already_active": {
                      "type": "boolean"
                    },
                    "activated_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "account": {
                      "$ref": "#/components/schemas/ServiceAccount"
                    },
                    "account_created": {
                      "type": "boolean",
                      "description": "True if the tenant account was created during this call; false if it was found-and-linked to an existing row by email."
                    },
                    "account_linked": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Key is not service tier, or `provision_account` body failed validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`provision_account` matched an existing account that is already claimed by a different consumer or has a Supabase Auth owner. The key is still activated; resolve the account manually and link via `POST /service/accounts/link`."
          }
        }
      }
    },
    "/service/register/send-otp": {
      "post": {
        "summary": "Self-service: send registration OTP — RETIRED 2026-05-19",
        "description": "**Retired.** Self-service registration moved to the developer portal at https://neighborhood-commons.org/developers/sign-up. The portal flow is a guided sign-up with magic-link login, MFA, and an operator review step (including the witnessed-with-evidence approval path). This endpoint now returns `410 ENDPOINT_RETIRED`. Existing service keys keep working — only the registration path moved.",
        "operationId": "serviceRegisterSendOtp",
        "deprecated": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 320
                  }
                }
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Endpoint retired (ENDPOINT_RETIRED). Use https://neighborhood-commons.org/developers/sign-up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/service/register/verify-otp": {
      "post": {
        "summary": "Self-service: verify OTP and receive a pending service key — RETIRED 2026-05-19",
        "description": "**Retired.** Self-service registration moved to the developer portal at https://neighborhood-commons.org/developers/sign-up. This endpoint now returns `410 ENDPOINT_RETIRED`. Existing service keys keep working — only the registration path moved.",
        "operationId": "serviceRegisterVerifyOtp",
        "deprecated": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "token",
                  "app_name",
                  "app_url",
                  "what_youre_building",
                  "verification_process"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 320
                  },
                  "token": {
                    "type": "string",
                    "minLength": 6,
                    "maxLength": 8
                  },
                  "app_name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "app_url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 500
                  },
                  "what_youre_building": {
                    "type": "string",
                    "minLength": 20,
                    "maxLength": 2000,
                    "description": "One paragraph. Who's the user, what's the integration shape."
                  },
                  "verification_process": {
                    "type": "string",
                    "minLength": 20,
                    "maxLength": 2000,
                    "description": "How you verify the organizations you onboard. Drives the verification_authority granted at activation."
                  },
                  "expected_first_week_writes": {
                    "type": "string",
                    "maxLength": 200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Endpoint retired (ENDPOINT_RETIRED). Use https://neighborhood-commons.org/developers/sign-up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/service/migrate-image-urls": {
      "post": {
        "summary": "One-time image URL migration",
        "description": "Rewrites image URLs across portal_accounts and events to direct R2 public URLs. Converts portal proxy paths and re-hosts external URLs (Google Places, gstatic, etc.) through the Sharp pipeline. Admin service key required. Requires `R2_PUBLIC_URL` to be configured on the server. Idempotent — safe to re-run.",
        "operationId": "serviceMigrateImageUrls",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Migration complete with per-category counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "migration": {
                      "type": "object",
                      "properties": {
                        "accounts": {
                          "type": "object",
                          "properties": {
                            "logo": {
                              "type": "integer"
                            },
                            "cover": {
                              "type": "integer"
                            },
                            "rehosted": {
                              "type": "integer"
                            }
                          }
                        },
                        "events": {
                          "type": "integer"
                        },
                        "errors": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "R2_PUBLIC_URL not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Non-admin service key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/service/approved-domains": {
      "get": {
        "summary": "List approved domains",
        "description": "List domains on the Service API URL allowlist (used for sanitizing event link URLs and similar). Admin service key required.",
        "operationId": "serviceListApprovedDomains",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Allowlisted domains",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "approved_domains": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApprovedDomain"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          }
        }
      },
      "post": {
        "summary": "Add an approved domain",
        "description": "Add a domain to the Service API URL allowlist. Resolves any pending approval request for the same domain. Admin service key required.",
        "operationId": "serviceAddApprovedDomain",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "domain"
                ],
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "Lowercase hostname only — no scheme, port, or path."
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Domain added"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          }
        }
      }
    },
    "/service/approved-domains/{domain}": {
      "delete": {
        "summary": "Remove an approved domain",
        "description": "Remove a domain from the allowlist. Future Service API submissions referencing URLs at this domain will queue for review. Admin service key required.",
        "operationId": "serviceRemoveApprovedDomain",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Domain removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          }
        }
      }
    },
    "/service/domain-approval-requests": {
      "get": {
        "summary": "List domain approval requests",
        "description": "Review queue of Service API URL submissions whose domain is not yet on the allowlist. Admin service key required.",
        "operationId": "serviceListDomainApprovalRequests",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "rejected"
              ],
              "default": "pending"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DomainApprovalRequest"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          }
        }
      }
    },
    "/service/domain-approval-requests/{id}/approve": {
      "post": {
        "summary": "Approve a domain approval request",
        "description": "Add the requested domain to the allowlist and mark the request approved. Admin service key required.",
        "operationId": "serviceApproveDomainRequest",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          },
          "404": {
            "description": "Request not found"
          },
          "409": {
            "description": "Request already reviewed"
          }
        }
      }
    },
    "/service/domain-approval-requests/{id}/reject": {
      "post": {
        "summary": "Reject a domain approval request",
        "description": "Mark the request rejected without allowlisting the domain. Admin service key required.",
        "operationId": "serviceRejectDomainRequest",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rejected"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          },
          "404": {
            "description": "Request not found"
          },
          "409": {
            "description": "Request already reviewed"
          }
        }
      }
    },
    "/places": {
      "get": {
        "summary": "List places",
        "description": "Public read of Place records (physical locations). Filter by region, geo proximity, or text search. No firehose: at least one structural filter recommended.",
        "operationId": "listPlaces",
        "parameters": [
          {
            "name": "near",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^-?\\d+\\.?\\d*,-?\\d+\\.?\\d*$"
            },
            "description": "Coordinates as lat,lng for proximity filtering"
          },
          {
            "name": "radius_km",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": 0.1,
              "maximum": 100,
              "default": 10
            }
          },
          {
            "name": "region",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by region slug"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated place list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "meta",
                    "places"
                  ],
                  "properties": {
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    },
                    "places": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Place"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/places/{id}": {
      "get": {
        "summary": "Get a single place",
        "operationId": "getPlace",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Place",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "place"
                  ],
                  "properties": {
                    "place": {
                      "$ref": "#/components/schemas/Place"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/organizations": {
      "get": {
        "summary": "List organizations",
        "description": "Public read of Organization records. Filters compose for opt-in consumption: `commercial` for the for-profit signal, `tag` for descriptive labels, `verified` and `verified_by` for trust, `near`/`radius_km` for proximity, `created_by_contributor` for app-source slicing. Classification (business vs. community group vs. collective etc.) is derived by consumers from `tags`, `commercial`, and structural signals — there is no `kind` enum.",
        "operationId": "listOrganizations",
        "parameters": [
          {
            "name": "verified",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "True to filter to verified organizations only."
          },
          {
            "name": "verified_by",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated app names. Filters to organizations verified by any of these apps."
          },
          {
            "name": "not_verified_by",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated app names. Excludes organizations verified by these apps."
          },
          {
            "name": "created_by_contributor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter to organizations contributed by this app — the publishing-app axis (`source.contributor`). Resolves against the registered `contributor_profile` slug; only `active` profiles match. Matches the `organizations.contributor_profile_id` snapshot set at write time (migration 090)."
          },
          {
            "name": "near",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^-?\\d+\\.?\\d*,-?\\d+\\.?\\d*$"
            }
          },
          {
            "name": "radius_km",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": 0.1,
              "maximum": 100,
              "default": 10
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated organization list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "meta",
                    "organizations"
                  ],
                  "properties": {
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    },
                    "organizations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Organization"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/organizations/{idOrSlug}": {
      "get": {
        "summary": "Get a single organization",
        "operationId": "getOrganization",
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Organization",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "organization"
                  ],
                  "properties": {
                    "organization": {
                      "$ref": "#/components/schemas/Organization"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/publishers": {
      "get": {
        "summary": "List publishers",
        "description": "Organizations that publish into the Commons — those with at least one event or active broadcast attributed to them. Same response shape as `/organizations`; the difference is a \"has published\" filter applied at the query layer.",
        "operationId": "listPublishers",
        "parameters": [
          {
            "name": "tag",
            "in": "query",
            "description": "Filter by organization tag. Repeatable for AND semantics.",
            "schema": {
              "oneOf": [
                { "type": "string", "maxLength": 50 },
                { "type": "array", "items": { "type": "string", "maxLength": 50 } }
              ]
            }
          },
          {
            "name": "commercial",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["true", "false"]
            }
          },
          {
            "name": "place_category",
            "in": "query",
            "description": "Filter to publishers whose primary place has the given OSM-sourced category.",
            "schema": { "type": "string", "maxLength": 50 }
          },
          {
            "name": "verified",
            "in": "query",
            "schema": { "type": "string", "enum": ["true", "false"] }
          },
          {
            "name": "verified_by",
            "in": "query",
            "schema": { "type": "string", "maxLength": 500 }
          },
          {
            "name": "not_verified_by",
            "in": "query",
            "schema": { "type": "string", "maxLength": 500 }
          },
          {
            "name": "near",
            "in": "query",
            "description": "lat,lng pair. Pairs with radius_km.",
            "schema": { "type": "string" }
          },
          {
            "name": "radius_km",
            "in": "query",
            "schema": { "type": "number", "minimum": 0.1, "maximum": 100 }
          },
          {
            "name": "q",
            "in": "query",
            "schema": { "type": "string", "maxLength": 200 }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": { "type": "integer", "minimum": 0, "default": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated publisher list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [ "meta", "publishers" ],
                  "properties": {
                    "meta": { "$ref": "#/components/schemas/Meta" },
                    "publishers": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Organization" }
                    }
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/publishers/{idOrSlug}": {
      "get": {
        "summary": "Get a single publisher",
        "operationId": "getPublisher",
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Publisher (organization shape)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [ "publisher" ],
                  "properties": {
                    "publisher": { "$ref": "#/components/schemas/Organization" }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/broadcasts": {
      "get": {
        "summary": "List broadcasts",
        "description": "Public read of active Broadcast records (ephemeral signals from organizations, pinned to places). Returns active-only by default; `near`+`radius_km` is the typical proximity filter. Apps surface broadcasts editorially — verification is a filter, not a Commons-side gate.",
        "operationId": "listBroadcasts",
        "parameters": [
          {
            "name": "near",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^-?\\d+\\.?\\d*,-?\\d+\\.?\\d*$"
            },
            "description": "Required for typical proximity reads."
          },
          {
            "name": "radius_km",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": 0.1,
              "maximum": 100,
              "default": 5
            }
          },
          {
            "name": "organization_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "verified",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Filter to broadcasts from verified organizations."
          },
          {
            "name": "verified_by",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "not_verified_by",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "created_by_contributor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter to publishers contributed by this app — the publishing-app axis (`source.contributor`). Resolves against the registered `contributor_profile` slug; only `active` profiles match."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated broadcast list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "meta",
                    "broadcasts"
                  ],
                  "properties": {
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    },
                    "broadcasts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Broadcast"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/broadcasts/{id}": {
      "get": {
        "summary": "Get a single broadcast",
        "operationId": "getBroadcast",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Broadcast",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast"
                  ],
                  "properties": {
                    "broadcast": {
                      "$ref": "#/components/schemas/Broadcast"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/series": {
      "get": {
        "summary": "List event series",
        "description": "Public read of event series records. A series is a recurring activity with its own public identity (name, slug, description, cover image). For the *instances* of a series, query `/events?series_id={id}` instead — this endpoint returns the series itself. See `docs/series-as-first-class.md` for the design.",
        "operationId": "listSeries",
        "parameters": [
          {
            "name": "organizer_org_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Filter to series run by a specific organization."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated series list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "meta",
                    "series"
                  ],
                  "properties": {
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    },
                    "series": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Series"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/series/{idOrSlug}": {
      "get": {
        "summary": "Get a single series",
        "description": "Fetch a series by UUID or slug. Used by consumer apps to render dedicated series pages (e.g., a subscribable Quizzo series). Returns the series identity, its organizer, recurrence, and the soonest upcoming instance.",
        "operationId": "getSeries",
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Either the series UUID or its stable slug."
          }
        ],
        "responses": {
          "200": {
            "description": "Series",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "series"
                  ],
                  "properties": {
                    "series": {
                      "$ref": "#/components/schemas/Series"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/contributors": {
      "get": {
        "summary": "List active contributor profiles",
        "description": "Active contributor profiles — the public-facing identity of each app or pipeline routing data into the Commons. Use this to show \"who's contributing here\" pages, or to look up a contributor by category. Only `status = 'active'` profiles surface; pending/suspended live on the operational side.",
        "operationId": "listContributors",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "schema": { "type": "string", "maxLength": 50 },
            "description": "Filter to profiles with the given category tag."
          },
          {
            "name": "q",
            "in": "query",
            "schema": { "type": "string", "maxLength": 200 },
            "description": "Free-text search over name, tagline, and description."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": { "type": "integer", "minimum": 0, "default": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated contributor list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["meta", "contributors"],
                  "properties": {
                    "meta": { "$ref": "#/components/schemas/Meta" },
                    "contributors": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/ContributorProfile" }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/contributors/{idOrSlug}": {
      "get": {
        "summary": "Get a single contributor profile",
        "description": "Fetch a single active contributor profile by UUID or slug. Used by consumer apps to render the tap-through splash card when a reader taps `via {contributor.name}` on an event.",
        "operationId": "getContributor",
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Either the contributor's UUID or its stable slug."
          }
        ],
        "responses": {
          "200": {
            "description": "Single contributor profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["contributor"],
                  "properties": {
                    "contributor": { "$ref": "#/components/schemas/ContributorProfile" }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No active contributor with this id/slug",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/lists": {
      "get": {
        "summary": "List curated lists",
        "description": "Public read of List records (curatorial selections of events, organizations, or places).",
        "operationId": "listLists",
        "parameters": [
          {
            "name": "curator_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of lists",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "meta",
                    "lists"
                  ],
                  "properties": {
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    },
                    "lists": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/List"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/lists/{idOrSlug}": {
      "get": {
        "summary": "Get a single list",
        "operationId": "getList",
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List with items hydrated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "list"
                  ],
                  "properties": {
                    "list": {
                      "$ref": "#/components/schemas/List"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/verifications/path": {
      "get": {
        "summary": "Discover the required verification path",
        "description": "Routing authority. Given a target and identifier, the Commons returns which submission endpoint to call next. Apps must call this first and follow the result — submission endpoints reject mismatches.",
        "operationId": "serviceVerificationPath",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "target_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "organization"
              ]
            },
            "description": "Always `organization`. Retained as a query parameter for forward compatibility; the substrate may admit additional target types in future minor versions."
          },
          {
            "name": "target_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "identifier_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "email"
              ]
            }
          },
          {
            "name": "identifier_value",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Path discovery result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerificationPathResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Target not found"
          }
        }
      }
    },
    "/service/verifications/challenges": {
      "post": {
        "summary": "Issue a verification challenge (auto-track)",
        "description": "Auto-track verification path: send a one-time code to the identifier (email). Used for business-domain emails on heavy-rigor organizations (commercial=true) and any email on light-rigor organizations (community groups, collectives, etc.). Personal-email domains for heavy-rigor targets are rejected here with WRONG_METHOD — call /service/verifications/manual instead.",
        "operationId": "serviceCreateVerificationChallenge",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerificationChallengeInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Challenge created; email sent (with the calling key's brand_config sender identity)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerificationChallengeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "WRONG_METHOD — this identifier requires the manual review path. Response includes redirect hint."
          }
        }
      }
    },
    "/service/verifications/challenges/{id}/confirm": {
      "post": {
        "summary": "Confirm a verification challenge",
        "description": "Submit the one-time code received via email. On match: an entry is added to the target's verified identifiers. On miss: attempt counter increments; challenge expires after configured limit.",
        "operationId": "serviceConfirmVerificationChallenge",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerificationConfirmInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verification outcome",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerificationOutcome"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Challenge not found or expired"
          },
          "409": {
            "description": "Challenge already consumed"
          }
        }
      }
    },
    "/service/verifications/manual": {
      "post": {
        "summary": "Submit manual-review verification evidence",
        "description": "Manual-review path: app submits structured evidence for human review. Required evidence schema is Commons-defined and validated at submit. Apps with `verification_authority` for the matching method auto-approve on submit; others queue for admin review.\n\nFor heavy-rigor org targets, this is the path for personal-email domains. For light-rigor targets, this path is rarely used (challenges suffice).",
        "operationId": "serviceSubmitManualVerification",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerificationManualInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verification outcome — verified (auto-approved by app authority), pending (queued), or rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerificationOutcome"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "WRONG_METHOD — this identifier should use the challenges path."
          }
        }
      }
    },
    "/service/verifications/pending": {
      "get": {
        "summary": "List pending verification reviews",
        "description": "Admin-tier read of the manual review queue. Only callers whose service key has `is_admin=true` (or has `verification_authority` covering the relevant method) see results.",
        "operationId": "serviceListPendingVerifications",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pending reviews",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reviews"
                  ],
                  "properties": {
                    "reviews": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PendingReview"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          }
        }
      }
    },
    "/service/verifications/pending/{id}/approve": {
      "post": {
        "summary": "Approve a pending verification review",
        "description": "Admin or authorized-app action. Inserts the verified identifier and marks the review approved. Approval criteria documented in `docs/verification-policy.md` — reviewer is bound by Commons-defined floor.",
        "operationId": "serviceApprovePendingVerification",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Approved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerificationOutcome"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Already reviewed"
          }
        }
      }
    },
    "/service/verifications/pending/{id}/reject": {
      "post": {
        "summary": "Reject a pending verification review",
        "description": "Admin or authorized-app action. Records the rejection with a Commons-defined reason code. Rejection is a permanent record but doesn't prevent future fresh submissions for the same identifier.",
        "operationId": "serviceRejectPendingVerification",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerificationRejectInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rejected"
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Admin access required"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Already reviewed"
          }
        }
      }
    },
    "/service/disputes": {
      "post": {
        "summary": "Record a dispute",
        "description": "v1 minimum: stores a dispute claim against a verified target or identifier for operator review. No automated action. Acts as the entry point for \"this verification is wrong\" feedback into the system.",
        "operationId": "serviceCreateDispute",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DisputeInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Dispute recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DisputeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/service/places": {
      "post": {
        "summary": "Create or look up a place",
        "description": "Idempotent on `googlePlaceId`: if a Place with that external ID exists, returns it instead of creating a duplicate. Without a googlePlaceId, creates a new Place row from the provided fields.",
        "operationId": "serviceCreatePlace",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlaceInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing place returned (idempotent match on googlePlaceId)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "place"
                  ],
                  "properties": {
                    "place": {
                      "$ref": "#/components/schemas/Place"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Place created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "place"
                  ],
                  "properties": {
                    "place": {
                      "$ref": "#/components/schemas/Place"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/service/organizations": {
      "post": {
        "summary": "Create an organization",
        "operationId": "serviceCreateOrganization",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Organization created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "organization"
                  ],
                  "properties": {
                    "organization": {
                      "$ref": "#/components/schemas/Organization"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Slug already in use"
          }
        }
      }
    },
    "/service/organizations/{id}": {
      "patch": {
        "summary": "Update an organization",
        "description": "Service-tier callers can update organizations linked to their key (via api_key_organization_links). Admin keys bypass the scoping check.",
        "operationId": "serviceUpdateOrganization",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "organization"
                  ],
                  "properties": {
                    "organization": {
                      "$ref": "#/components/schemas/Organization"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not linked to this organization"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/broadcasts": {
      "post": {
        "summary": "Create a broadcast",
        "description": "Create an ephemeral signal pinned to a Place. The Organization must be linked to the calling key (via api_key_organization_links) unless the key has admin privileges. Verification is NOT required at create time — apps filter on verified status when surfacing.",
        "operationId": "serviceCreateBroadcast",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BroadcastInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Broadcast created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast"
                  ],
                  "properties": {
                    "broadcast": {
                      "$ref": "#/components/schemas/Broadcast"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not linked to this organization"
          }
        }
      }
    },
    "/service/broadcasts/{id}/retract": {
      "post": {
        "summary": "Retract an active broadcast",
        "description": "Marks the broadcast retracted. Status changes to `retracted` and `retracted_at` is set. Idempotent.",
        "operationId": "serviceRetractBroadcast",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Retracted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast"
                  ],
                  "properties": {
                    "broadcast": {
                      "$ref": "#/components/schemas/Broadcast"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not linked to this organization"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/lists": {
      "post": {
        "summary": "Create a list",
        "operationId": "serviceCreateList",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "List created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "list"
                  ],
                  "properties": {
                    "list": {
                      "$ref": "#/components/schemas/List"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not linked to the curator entity"
          }
        }
      }
    },
    "/service/lists/{id}": {
      "patch": {
        "summary": "Update a list (metadata only)",
        "description": "Updates list name/description/slug. Items are managed via /service/lists/{id}/items endpoints.",
        "operationId": "serviceUpdateList",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "list"
                  ],
                  "properties": {
                    "list": {
                      "$ref": "#/components/schemas/List"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not linked to the curator entity"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/lists/{id}/items": {
      "post": {
        "summary": "Add an item to a list",
        "description": "Adds an event, organization, or place to the list at the specified position. If position conflicts with existing items, behavior is configurable (default: shift existing items down).",
        "operationId": "serviceAddListItem",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListItemInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Item added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "item"
                  ],
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/ListItem"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not linked to the curator entity"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/lists/{id}/items/{position}": {
      "delete": {
        "summary": "Remove an item from a list",
        "operationId": "serviceRemoveListItem",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "position",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not linked to the curator entity"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/accounts/link": {
      "post": {
        "summary": "Find-or-create a portal account by email (tenant claim)",
        "description": "Operational tenant claim only. Establishes a portal_account row with the given email and marks it claimed by the calling app. Does NOT grant writeable scope — that flows through `api_key_organization_links`, established by `POST /service/organizations/link` or auto-linked when the calling key creates an organization via `POST /service/organizations`.\n\nIdempotent: calling with the same email returns the existing account.\n\n**Defense-in-depth on claim**: the endpoint refuses (409 CONFLICT) to claim an account that has `auth_user_id` set (a Supabase Auth owner) or that has been claimed under a different `claimed_by` identifier than the request. Admin keys bypass both checks.",
        "operationId": "serviceLinkAccount",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254,
                    "description": "Email identifier for the operational tenant row. Use a sentinel address on a domain you control (e.g. `tenant@no-reply.your-domain.com`). Lookup is case-insensitive."
                  },
                  "claimed_by": {
                    "type": "string",
                    "maxLength": 50,
                    "description": "Short slug recording which app claimed this account. Persisted on first claim; subsequent link attempts under a different `claimed_by` are refused with 409 CONFLICT. Defaults to `\"api\"` if omitted."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account existed; claim is in place (idempotent re-claim)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "account",
                    "created"
                  ],
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/ServiceAccount"
                    },
                    "created": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Account created and claimed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "account",
                    "created"
                  ],
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/ServiceAccount"
                    },
                    "created": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Account is already claimed under a different `claimed_by` or has an authenticated owner (`auth_user_id` set). Admin keys bypass these checks."
          }
        }
      }
    },
    "/service/organizations/link": {
      "post": {
        "summary": "Link an existing organization to this service key",
        "description": "Establishes a relationship between the calling service key and an existing Organization. After linking, the key may write to that organization's data (events, broadcasts, identifier verifications, lists curated by it). Idempotent: calling on an already-linked organization returns 200 with the existing link.",
        "operationId": "serviceLinkOrganization",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "organizationId"
                ],
                "properties": {
                  "organizationId": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already linked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "organization"
                  ],
                  "properties": {
                    "organization": {
                      "$ref": "#/components/schemas/Organization"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Linked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "organization"
                  ],
                  "properties": {
                    "organization": {
                      "$ref": "#/components/schemas/Organization"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/organizations/{id}/logo": {
      "post": {
        "summary": "Upload organization logo",
        "description": "Multipart upload. Validates magic bytes, re-encodes through Sharp, stores in R2. Updates organization.logo to the new URL.",
        "operationId": "serviceUploadOrganizationLogo",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Logo uploaded; new URL written to organization.logo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "organization"
                  ],
                  "properties": {
                    "organization": {
                      "$ref": "#/components/schemas/Organization"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not linked to this organization"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/organizations/{id}/image": {
      "post": {
        "summary": "Upload organization hero image",
        "description": "Multipart upload of a hero/cover image. Same pipeline as logo upload. Updates organization.image.",
        "operationId": "serviceUploadOrganizationImage",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Image uploaded; new URL written to organization.image",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "organization"
                  ],
                  "properties": {
                    "organization": {
                      "$ref": "#/components/schemas/Organization"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not linked to this organization"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/service/events/{id}/organizer": {
      "patch": {
        "summary": "Re-attribute an event to a different organizer",
        "description": "Re-attribute an event to a different organization.\n\nAuthorization (non-admin keys): the caller's key must be linked to both the event's **current** `organizer_org_id` AND the **target** organization via `api_key_organization_links`. Re-attribution can't conjure events for an org the caller doesn't control. Witnessed-evidence keys (`source_method='witnessed'` + `witness_authority=true`) bypass the current-organizer link check.\n\nAdmin keys bypass all linkage checks.",
        "operationId": "serviceAssignEventOrganizer",
        "security": [
          {
            "serviceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["organizerOrganizationId"],
                "properties": {
                  "organizerOrganizationId": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Event organizer updated"
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Caller's key is not linked to the event's current organizer organization OR not linked to the target organization."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Meta": {
        "type": "object",
        "required": [
          "total",
          "limit",
          "offset",
          "spec",
          "license"
        ],
        "properties": {
          "total": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "spec": {
            "type": "string",
            "example": "neighborhood-api-v0.2",
            "description": "The upstream Neighborhood API spec this implementation conforms to (distinct from the implementation version of this specific Commons instance, which is in `info.version` of `/openapi.json` and at `/meta` under `implementation_version`)."
          },
          "license": {
            "type": "string",
            "example": "CC-BY-4.0"
          }
        }
      },
      "Event": {
        "type": "object",
        "required": [
          "id",
          "name",
          "start",
          "timezone",
          "category",
          "organizer",
          "source"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "start": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 with timezone offset"
          },
          "end": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "timezone": {
            "type": "string",
            "description": "IANA timezone name",
            "example": "America/New_York"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "place_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "location": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "lat": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "lng": {
                "type": [
                  "number",
                  "null"
                ]
              }
            }
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "images": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "organizer": {
            "type": "object",
            "description": "Organizer is always an organization reference. `events.organizer_org_id` is NOT NULL, so id/slug are always present. `verified` is hydrated from `organization_verifications`. `phone` is retained as a vestigial field — always null.",
            "required": ["id", "slug", "name", "verified"],
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "slug": { "type": "string" },
              "name": { "type": "string" },
              "verified": {
                "type": "boolean",
                "description": "True if the organizer has at least one active verification record."
              },
              "phone": {
                "type": ["string", "null"],
                "description": "Legacy field. Always null in v2."
              }
            }
          },
          "cost": {
            "type": [
              "string",
              "null"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "wheelchair_accessible": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "open_window": {
            "type": "boolean",
            "description": "True for come-and-go events (happy hour, open swim, market). When true, feeds keep the event visible until end_time (or start + 3h if no end). When false, the event disappears from feeds at start time."
          },
          "capacity": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10000,
            "description": "Informational max attendance. Commons does NOT track signups or enforce the cap. Ticketing lives in url."
          },
          "rsvp": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "recommended",
              "required",
              null
            ],
            "description": "Whether RSVP is a thing for this event. Commons does not manage RSVPs."
          },
          "series_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "series_instance_number": {
            "type": [
              "integer",
              "null"
            ]
          },
          "series_instance_count": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Total instances in the series. Populated on /events and /events/{id}. Null for one-offs or in contexts where hydration is skipped (webhook payloads)."
          },
          "recurrence": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "properties": {
                  "rrule": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "first_party": {
            "type": "boolean",
            "description": "Authority tier. `true` if the event's organizer had at least one active verified identifier at the time the event was inserted — meaning the event was posted by the verified organization itself. `false` for events whose organizer hadn't completed verification at write time (typically proxied or witnessed events). Computed server-side; not a writable input. Filterable via `?first_party=true|false` on `GET /events`. Related but distinct from `source.method`: `first_party` is a snapshot of verification state at write time; `source.method` describes the authority chain."
          },
          "event_image_focal_y": {
            "type": [
              "number",
              "null"
            ],
            "description": "Vertical focal point for image cropping (0=top, 1=bottom, 0.5=center)"
          },
          "tmdb_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "TMDB film ID (themoviedb.org), used to cluster film-category events across theaters and dates. Recommended when category includes 'film'. Format: numeric string (e.g. '1064713' for Anora). Consumers fetch metadata at https://api.themoviedb.org/3/movie/{tmdb_id}. Films not in TMDB leave this null and appear individually instead of clustering. Filterable on `GET /events?tmdb_id={id}`.",
            "example": "1064713"
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          }
        }
      },
      "Source": {
        "type": "object",
        "description": "Event provenance under the four-role frame (see docs/four-roles.md). Carries the contributor identity (which ecosystem participant routed the event in), the method (authority shape: self_asserted, proxied, witnessed), the source URL when applicable (proxied events), plus collected_at and license. Does not carry a `publisher` field — the role 'who is this from?' is filled by the top-level `organizer.name`.",
        "required": [
          "method",
          "url",
          "contributor",
          "collected_at",
          "license"
        ],
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "self_asserted",
              "proxied",
              "witnessed"
            ],
            "description": "Standard provenance method (docs/provenance.md). `self_asserted`: the organizer asserted this via the contributor (entity-runs-it). `proxied`: the contributor extracted this from a public URL (pipeline-proxies). `witnessed`: the contributor observed this with documentary evidence under a collective identity (witnessed-with-evidence). Drives rendering rules — consumers typically suppress the 'via {contributor}' line on `witnessed` events because the organizer is the collective constituted by the contributor."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "When method is `proxied`, the public URL the contributor extracted from (for transparency). Null otherwise."
          },
          "contributor": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": ["name", "url", "slug", "logo_url", "description", "profile_url"],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Public-facing name of the contributing app/pipeline."
                  },
                  "url": {
                    "type": ["string", "null"],
                    "format": "uri",
                    "description": "Contributor website (`app_url` from the registered profile, or the legacy snapshot URL for pre-3.1 events)."
                  },
                  "slug": {
                    "type": ["string", "null"],
                    "description": "Stable cross-key slug from the registered contributor_profile. Use this to deep-link the tap-through splash card. Null for pre-3.1 events that fell back to the snapshot."
                  },
                  "logo_url": {
                    "type": ["string", "null"],
                    "format": "uri",
                    "description": "Logo image for the splash card. Null when no profile is linked."
                  },
                  "description": {
                    "type": ["string", "null"],
                    "description": "Profile description (markdown-aware). Null when no profile is linked."
                  },
                  "profile_url": {
                    "type": ["string", "null"],
                    "description": "Relative URL of the public profile resource, e.g. `/v1/contributors/merrie`. Fetch for the full profile shape. Null when no profile is linked."
                  }
                }
              }
            ],
            "description": "The ecosystem participant that routed this event into the Commons. Editorial public-facing identity — separate from the operational API key. When linked to a registered `contributor_profiles` row (3.1+), surfaces the full profile (slug + name + url + logo_url + description + profile_url). Pre-3.1 events fall back to the legacy {name, url} snapshot with the new fields as null. Null entirely for legacy rows without any contributor attribution."
          },
          "collected_at": {
            "type": "string",
            "format": "date-time"
          },
          "license": {
            "type": "string",
            "example": "CC BY 4.0"
          }
        }
      },
      "ContributorProfile": {
        "type": "object",
        "description": "The public-facing identity of a contributing app — the splash card data a consumer app renders when a reader taps `via {contributor.name}`. Profiles are registered via the developer dashboard at `/developers`; the slug is stable across api_key rotation. Only `status = 'active'` profiles surface here.",
        "required": ["id", "slug", "name", "created_at", "updated_at"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string",
            "description": "Lowercase alphanumeric + hyphens, 1-100 chars. Stable cross-key identifier."
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "tagline": {
            "type": ["string", "null"],
            "description": "One-line description, ~80 chars."
          },
          "description": {
            "type": ["string", "null"],
            "description": "Longer description, ~2000 chars. Markdown-aware on the rendering side."
          },
          "who_its_for": {
            "type": ["string", "null"],
            "description": "Audience description, ~500 chars."
          },
          "app_url": {
            "type": ["string", "null"],
            "format": "uri",
            "description": "Public marketing or app URL the splash card links to."
          },
          "logo_url": {
            "type": ["string", "null"],
            "format": "uri",
            "description": "R2-served logo image."
          },
          "category": {
            "type": ["string", "null"],
            "description": "Optional grouping tag (free-form)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ServiceEvent": {
        "type": "object",
        "description": "Event response shape for service-tier and portal endpoints. Uses DB-flavored field names (title, event_date, start_time, etc.) — distinct from the public read Event shape. Returned by GET /service/events/{id}, PATCH /service/events/{id}, and the portal API.",
        "required": [
          "id",
          "title",
          "event_timezone",
          "status",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "portal_account_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "title": {
            "type": "string",
            "description": "Equivalent to `name` in the public Event shape"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "venue_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Equivalent to `location.name` in the public Event shape"
          },
          "address": {
            "type": [
              "string",
              "null"
            ]
          },
          "place_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "latitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "longitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "event_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Local date (YYYY-MM-DD) in event_timezone"
          },
          "start_time": {
            "type": [
              "string",
              "null"
            ],
            "description": "Local time (HH:MM:SS) in event_timezone"
          },
          "end_time": {
            "type": [
              "string",
              "null"
            ],
            "description": "Local time (HH:MM:SS) in event_timezone"
          },
          "event_timezone": {
            "type": "string",
            "description": "IANA timezone name"
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Single category slug (the public Event shape wraps this in an array)"
          },
          "custom_category": {
            "type": [
              "string",
              "null"
            ]
          },
          "recurrence": {
            "type": [
              "string",
              "null"
            ],
            "description": "iCal RRULE string, or null for one-offs"
          },
          "price": {
            "type": [
              "string",
              "null"
            ],
            "description": "Equivalent to `cost` in the public Event shape"
          },
          "ticket_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Equivalent to `url` in the public Event shape"
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "image_focal_y": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "default": 0.5
          },
          "open_window": {
            "type": "boolean",
            "default": false
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "wheelchair_accessible": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "capacity": {
            "type": [
              "integer",
              "null"
            ]
          },
          "rsvp": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "recommended",
              "required",
              null
            ]
          },
          "status": {
            "type": "string"
          },
          "series_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "series_instance_number": {
            "type": [
              "integer",
              "null"
            ]
          },
          "first_party": {
            "type": "boolean",
            "default": false,
            "description": "Authority tier. Computed server-side from the organizer's verification state at insert time."
          },
          "source_feed_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "When source_method is `proxied`, the public URL the contributor extracted from. Null otherwise."
          },
          "tmdb_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "TMDB film ID for clustering film-category events. See Event.tmdb_id."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ServiceAccount": {
        "type": "object",
        "description": "Operational portal_account shell — email, claim state, status, timestamps. Business profile (name, address, logo, hours, etc.) lives on organizations.",
        "required": [
          "id",
          "email",
          "status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "auth_user_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "suspended",
              "pending",
              "rejected"
            ]
          },
          "claimed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "claimed_by": {
            "type": ["string", "null"],
            "description": "Slug identifying which consumer app claimed this tenant row."
          },
          "last_login_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "event_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "event.created",
                "event.updated",
                "event.deleted",
                "event.series_created",
                "event.image_processed",
                "series.updated",
                "series.deleted"
              ]
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ApprovedDomain": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "description": "Lowercase hostname (e.g. example.com)"
          },
          "added_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "added_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DomainApprovalRequest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "domain": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The full URL that triggered the review"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected"
            ]
          },
          "reviewed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ServiceEventInput": {
        "type": "object",
        "description": "Input schema for the Service API. Uses Neighborhood API friendly-shape field names (name, start, location, url, cost) — symmetric with the read schema. Recurrence is optional; omit for one-off events.\n\n`organizerOrganizationId` is required (the constrained-publishing authority anchor). Authority is satisfied by one of: the calling service key linked to that organization via `api_key_organization_links` (`source_method='self_asserted'`, default); `source_method='proxied'` from a key with `proxy_authority=true` plus a `source_feed_url` (pipeline-proxies a public page; org-link bypassed); or `source_method='witnessed'` from a key with `witness_authority=true` (collective-evidence path).",
        "required": [
          "organizerOrganizationId",
          "name",
          "start",
          "timezone",
          "category",
          "location"
        ],
        "properties": {
          "organizerOrganizationId": {
            "type": "string",
            "format": "uuid",
            "description": "The organization this event is published for. Required. Caller's service key must be linked to this org via `api_key_organization_links` (or use the witnessed-evidence path)."
          },
          "source_method": {
            "type": "string",
            "enum": ["self_asserted", "proxied", "witnessed"],
            "default": "self_asserted",
            "description": "Caller-set provenance method (docs/four-roles.md, docs/provenance.md). `self_asserted` (default): the organizer asserted this event; requires `api_key_organization_links` linkage. `proxied`: pipeline-proxies path — the contributor extracted this from a public page; requires `api_keys.proxy_authority=true` (or an admin key) and a `source_feed_url`; the organizer stays the scraped real-world entity and the org-link check is bypassed. `witnessed`: collective-evidence path; requires `api_keys.witness_authority=true` and is attributed to a collective publisher organization."
          },
          "source_feed_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2000,
            "description": "The public URL the contributor extracted this event from. Required when `source_method='proxied'`; ignored otherwise. Preserves the data lineage (docs/four-roles.md Path 2)."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "start": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 with timezone offset, e.g. 2026-05-01T19:00:00-04:00"
          },
          "end": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 with timezone offset (optional)"
          },
          "timezone": {
            "type": "string",
            "description": "IANA timezone name (e.g. America/New_York)"
          },
          "category": {
            "type": "string"
          },
          "custom_category": {
            "type": "string",
            "maxLength": 50
          },
          "location": {
            "type": "object",
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200
              },
              "address": {
                "type": "string",
                "maxLength": 500
              },
              "lat": {
                "type": "number",
                "minimum": -90,
                "maximum": 90
              },
              "lng": {
                "type": "number",
                "minimum": -180,
                "maximum": 180
              },
              "place_id": {
                "type": "string",
                "maxLength": 500
              }
            }
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "cost": {
            "type": "string",
            "maxLength": 100
          },
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2000
          },
          "image_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2000,
            "description": "Source image URL; fetched, re-encoded through Sharp, and hosted by Commons."
          },
          "recurrence": {
            "type": "string",
            "description": "RRULE string (e.g. FREQ=WEEKLY;COUNT=12). Omit for one-off events."
          },
          "instance_count": {
            "type": "integer",
            "minimum": 0,
            "maximum": 52
          },
          "series": {
            "type": "object",
            "description": "Series-level identity, used only when `recurrence` is set. When omitted, the series name defaults to the event name and the slug is server-derived. Pass explicit values to give the series a distinct public identity (e.g., \"Fishtown Quizzo\" with slug `fishtown-quizzo`, separate from any individual instance's name). See `docs/series-as-first-class.md`.",
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200
              },
              "slug": {
                "type": "string",
                "pattern": "^[a-z0-9][a-z0-9-]{0,99}$",
                "description": "Globally unique. Server returns 409 on collision."
              },
              "description": {
                "type": "string",
                "maxLength": 2000
              },
              "cover_image_url": {
                "type": "string",
                "format": "uri",
                "maxLength": 2000
              }
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 100
            },
            "maxItems": 15
          },
          "wheelchair_accessible": {
            "type": "boolean"
          },
          "open_window": {
            "type": "boolean",
            "description": "True for come-and-go events. Default false."
          },
          "capacity": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10000,
            "description": "Informational only; Commons does not enforce."
          },
          "rsvp": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "recommended",
              "required",
              null
            ],
            "description": "Whether RSVP is a thing for this event. Commons does not manage RSVPs."
          },
          "contributor": {
            "type": "object",
            "required": [
              "name"
            ],
            "description": "Per-event attribution for the app/tool that pushed this event into the Commons. Distinct from `source.publisher`, which names the organization the event is FROM. v2.1: when omitted, the server auto-fills `contributor.name` from the calling service key's `brand_config.app_name` so every consumer's events carry ecosystem attribution by default. Admin keys skip the auto-fill (they act on behalf of, not as ecosystem contributors). Send `null` on PATCH to clear an existing override.",
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200
              },
              "url": {
                "type": "string",
                "format": "uri",
                "maxLength": 2000
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "published",
              "pending_review",
              "draft"
            ]
          },
          "venue_id": {
            "type": "string",
            "format": "uuid"
          },
          "external_id": {
            "type": "string",
            "maxLength": 500
          },
          "tmdb_id": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 50,
            "description": "TMDB film ID for clustering film-category events. Send numeric string (e.g. '1064713') when category includes 'film'. See Event.tmdb_id for the read-side semantics."
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "description": "Machine-readable error identifier. Groups:\n\n- **Auth**: `ACCESS_DENIED`, `ACCOUNT_DISABLED`, `ACCOUNT_REQUIRED`, `API_KEY_REQUIRED`, `CAPTCHA_FAILED`, `FORBIDDEN`, `INSUFFICIENT_TIER`, `INVALID_API_KEY`, `INVALID_OTP`, `KEY_NOT_LINKED`, `KEY_PENDING`, `NOT_LINKED`, `NO_OWNER`, `UNAUTHORIZED`\n- **Validation / resource**: `ALREADY_EXISTS`, `BATCH_ALREADY_SUBMITTED`, `CONFLICT`, `DUPLICATE`, `INVALID_STATE`, `NOT_FOUND`, `VALIDATION_ERROR`\n- **URL / domain**: `BLOCKED_HOSTNAME`, `DOMAIN_PENDING_REVIEW`, `INVALID_SCHEME`, `INVALID_URL`, `INVALID_WEBHOOK_URL`, `IP_LITERAL`, `URL_CREDENTIALS`\n- **Rate limit / quota**: `PAYLOAD_TOO_LARGE`, `RATE_LIMIT`, `SUBSCRIPTION_LIMIT`\n- **CSV / import**: `CSV_EMPTY`, `CSV_INVALID`, `CSV_TOO_LARGE`, `NO_VALID_ROWS`\n- **Verification**: `IDENTIFIER_DISPUTED`, `IMPOSTER_SIGNALS`, `INSUFFICIENT_EVIDENCE`, `OUT_OF_POLICY`, `WRONG_METHOD`\n- **Media / content**: `IMAGE_NOT_PERMITTED`\n- **Server / infrastructure**: `DATABASE_ERROR`, `INTERNAL_ERROR`, `SERVER_ERROR`, `SERVICE_UNAVAILABLE`, `UPSTREAM_ERROR`",
        "enum": [
          "ACCESS_DENIED",
          "ACCOUNT_DISABLED",
          "ACCOUNT_REQUIRED",
          "ALREADY_EXISTS",
          "API_KEY_REQUIRED",
          "BATCH_ALREADY_SUBMITTED",
          "BLOCKED_HOSTNAME",
          "CAPTCHA_FAILED",
          "CONFLICT",
          "CSV_EMPTY",
          "CSV_INVALID",
          "CSV_TOO_LARGE",
          "DATABASE_ERROR",
          "DOMAIN_PENDING_REVIEW",
          "DUPLICATE",
          "FORBIDDEN",
          "IDENTIFIER_DISPUTED",
          "IMAGE_NOT_PERMITTED",
          "IMPOSTER_SIGNALS",
          "INSUFFICIENT_EVIDENCE",
          "INSUFFICIENT_TIER",
          "INTERNAL_ERROR",
          "INVALID_API_KEY",
          "INVALID_OTP",
          "INVALID_SCHEME",
          "INVALID_STATE",
          "INVALID_URL",
          "INVALID_WEBHOOK_URL",
          "IP_LITERAL",
          "KEY_NOT_LINKED",
          "KEY_PENDING",
          "NO_OWNER",
          "NO_VALID_ROWS",
          "NOT_FOUND",
          "NOT_LINKED",
          "OUT_OF_POLICY",
          "PAYLOAD_TOO_LARGE",
          "RATE_LIMIT",
          "SERVER_ERROR",
          "SERVICE_UNAVAILABLE",
          "SUBSCRIPTION_LIMIT",
          "UNAUTHORIZED",
          "UPSTREAM_ERROR",
          "URL_CREDENTIALS",
          "VALIDATION_ERROR",
          "WRONG_METHOD"
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "$ref": "#/components/schemas/ErrorCode"
              },
              "message": {
                "type": "string"
              },
              "domain": {
                "type": "string",
                "description": "Present when code is DOMAIN_PENDING_REVIEW"
              }
            }
          }
        }
      },
      "PostalAddress": {
        "type": "object",
        "description": "Schema.org PostalAddress. Used to represent structured addresses on Place and related types.",
        "properties": {
          "streetAddress": {
            "type": [
              "string",
              "null"
            ]
          },
          "addressLocality": {
            "type": [
              "string",
              "null"
            ],
            "description": "City / locality, e.g. \"Philadelphia\""
          },
          "addressRegion": {
            "type": [
              "string",
              "null"
            ],
            "description": "State or region. US: 2-letter code (e.g. \"PA\")"
          },
          "postalCode": {
            "type": [
              "string",
              "null"
            ]
          },
          "addressCountry": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2 code",
            "default": "US"
          }
        }
      },
      "GeoCoordinates": {
        "type": "object",
        "description": "Schema.org GeoCoordinates.",
        "required": [
          "latitude",
          "longitude"
        ],
        "properties": {
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          }
        }
      },
      "IdentifierValue": {
        "type": "object",
        "description": "Schema.org PropertyValue used in identifier arrays. Carries external IDs (e.g., googlePlaceId) alongside our internal UUIDs.",
        "required": [
          "propertyID",
          "value"
        ],
        "properties": {
          "propertyID": {
            "type": "string",
            "example": "googlePlaceId"
          },
          "value": {
            "type": "string"
          }
        }
      },
      "OpeningHoursEntry": {
        "type": "object",
        "description": "Schema.org OpeningHoursSpecification entry. One per day-range. Times are 24h HH:MM in the entity's local timezone.",
        "properties": {
          "dayOfWeek": {
            "type": [
              "string",
              "array"
            ],
            "description": "Single day or array of days. Schema.org-canonical: full names.",
            "items": {
              "type": "string",
              "enum": [
                "Monday",
                "Tuesday",
                "Wednesday",
                "Thursday",
                "Friday",
                "Saturday",
                "Sunday"
              ]
            }
          },
          "opens": {
            "type": "string",
            "description": "24h time, e.g. \"09:00\""
          },
          "closes": {
            "type": "string",
            "description": "24h time, e.g. \"17:00\""
          }
        }
      },
      "Place": {
        "type": "object",
        "description": "Schema.org Place. A physical location. Identity is the address; the stable cross-source key is `googlePlaceId` (the one Google datum permitted indefinite storage). Categories come from OpenStreetMap (ODbL-licensed, attributed) — see `placeCategories` and `categorySource`. Google response data beyond `googlePlaceId` is never persisted into these columns.",
        "required": [
          "id",
          "name",
          "geo"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PostalAddress"
              },
              {
                "type": "null"
              }
            ]
          },
          "geo": {
            "$ref": "#/components/schemas/GeoCoordinates"
          },
          "identifier": {
            "type": "array",
            "description": "External IDs. Common entry: { propertyID: \"googlePlaceId\", value: \"ChIJ...\" }",
            "items": {
              "$ref": "#/components/schemas/IdentifierValue"
            }
          },
          "placeCategories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Place categorization (e.g., \"cafe\", \"live_music_venue\"). Sourced from OpenStreetMap by default; never from Google response data."
          },
          "categorySource": {
            "type": ["string", "null"],
            "enum": ["osm", "admin_review", "publisher_declaration", null],
            "description": "Provenance of `placeCategories`. `osm` (default), `admin_review` (operator added), `publisher_declaration` (verified publisher refined)."
          },
          "region_slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Slug of the region this place falls within, if any"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PlaceInput": {
        "type": "object",
        "description": "Input for /service/places. Idempotent on googlePlaceId: if a Place with that ID already exists, returns it instead of creating a duplicate.",
        "required": [
          "name",
          "geo"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "googlePlaceId": {
            "type": "string",
            "maxLength": 500,
            "description": "If provided, lookup-or-create idempotently"
          },
          "address": {
            "$ref": "#/components/schemas/PostalAddress"
          },
          "geo": {
            "$ref": "#/components/schemas/GeoCoordinates"
          }
        }
      },
      "Verification": {
        "type": "object",
        "description": "Verification block exposed on verified Organization responses. Anchors Type A authority on organizations only — there is no cross-app reputation graph.",
        "required": [
          "method",
          "verifiedAt",
          "verifiedByApp"
        ],
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "domain_email_loop",
              "manual_review"
            ]
          },
          "verifiedVia": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "in_person",
              "video_call",
              null
            ],
            "description": "Present for manual_review only."
          },
          "verifiedAt": {
            "type": "string",
            "format": "date-time"
          },
          "verifiedByApp": {
            "type": "string",
            "description": "Stable app name (snapshot at approval time, immune to key rotation)."
          }
        }
      },
      "VerifiedIdentifier": {
        "type": "object",
        "description": "A specific verified identifier on an organization. Service-tier responses include full value; public responses may mask.",
        "required": [
          "identifierType",
          "method",
          "verifiedAt",
          "verifiedByApp"
        ],
        "properties": {
          "identifierType": {
            "type": "string",
            "enum": [
              "email"
            ]
          },
          "identifierValue": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full value for service-tier reads; null or partial for public reads."
          },
          "method": {
            "type": "string",
            "enum": [
              "domain_email_loop",
              "manual_review"
            ]
          },
          "verifiedAt": {
            "type": "string",
            "format": "date-time"
          },
          "verifiedByApp": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "revoked"
            ]
          }
        }
      },
      "Organization": {
        "type": "object",
        "description": "Schema.org Organization. The unified entity primitive — businesses, community groups, nonprofits, collectives, solo operators (organizations-of-one). Classification emerges from `tags` (descriptive labels), `commercial` (for-profit boolean), and structural signals (primary place, event history). The legacy `kind` enum was retired because it mixed structural facts, vibes, and legal status into one false choice. `additionalType` is derived structurally — `LocalBusiness` when a primary place is set, plain `Organization` otherwise. `method` carries the provenance under the standard four-value vocabulary (docs/provenance.md).",
        "required": [
          "id",
          "slug",
          "name",
          "tags",
          "method"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "method": {
            "type": "string",
            "enum": [
              "self_asserted",
              "proxied",
              "witnessed",
              "seeded"
            ],
            "description": "Standard provenance method (docs/provenance.md). `self_asserted`: first-party authority (the org has claimed and verified itself). `proxied`: extracted from a public source (e.g. a business registry). `witnessed`: collective-evidence path. `seeded`: bulk-imported, awaiting first-party uptake. Consumers can filter for `self_asserted` orgs to surface only first-party-asserted records."
          },
          "legalName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Official registered name. Optional, primarily for verified businesses."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 50
            },
            "description": "Descriptive labels — free-form within format rules. Recommended starter vocabulary at `/v1/meta/tags` (when shipped). Not a hard taxonomy; consumer apps filter on whatever tags appear in practice."
          },
          "commercial": {
            "type": ["boolean", "null"],
            "description": "For-profit (true) vs. non-profit/community (false). Null = unspecified. Replaces the structural-axis component of the legacy `kind` enum."
          },
          "additionalType": {
            "type": "string",
            "format": "uri",
            "description": "Schema.org type URL derived from structural data: `https://schema.org/LocalBusiness` when the organization has a primary place, `https://schema.org/Organization` otherwise. Apps wanting richer subtyping derive from `tags` or `place_categories`."
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "logo": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "image": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "telephone": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email"
          },
          "sameAs": {
            "type": "array",
            "description": "URLs that unambiguously identify this organization (Wikipedia, Wikidata, social profiles).",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "openingHoursSpecification": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/OpeningHoursEntry"
            }
          },
          "location": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Place"
              },
              {
                "type": "null"
              }
            ],
            "description": "The Place this Organization primarily operates at. Null for organizations without a fixed location (touring, online-only)."
          },
          "verified": {
            "type": "boolean"
          },
          "verification": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Verification"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrganizationInput": {
        "type": "object",
        "description": "Input shape for creating or updating an Organization. Classification is via `tags` (descriptive labels) and `commercial` (for-profit signal) — there is no `kind` field. Calling key auto-links to the new organization on create via `api_key_organization_links`; use `POST /service/organizations/link` to link to an existing org.",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "slug": {
            "type": "string",
            "maxLength": 100,
            "description": "Optional; auto-generated from name if omitted."
          },
          "legalName": {
            "type": "string",
            "maxLength": 200
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 50
            },
            "maxItems": 15,
            "description": "Descriptive labels. Free-form within format rules; see `/v1/meta/tags` for a recommended starter vocabulary."
          },
          "commercial": {
            "type": ["boolean", "null"],
            "description": "For-profit (true) vs. non-profit/community (false). Null = unspecified."
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2000
          },
          "logo": {
            "type": "string",
            "format": "uri",
            "maxLength": 2000
          },
          "image": {
            "type": "string",
            "format": "uri",
            "maxLength": 2000
          },
          "telephone": {
            "type": "string",
            "maxLength": 50
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "sameAs": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "maxItems": 20
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 50
            },
            "maxItems": 20
          },
          "openingHoursSpecification": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OpeningHoursEntry"
            }
          },
          "primaryPlaceId": {
            "type": "string",
            "format": "uuid",
            "description": "Reference to a Place row. Optional — drives `additionalType` derivation (`LocalBusiness` if set, plain `Organization` otherwise)."
          }
        }
      },
      "Broadcast": {
        "type": "object",
        "description": "Ephemeral real-time signal from an Organization, pinned to a Place. Maximum lifetime 24h. No direct Schema.org analog; conventions borrowed from SpecialAnnouncement (`datePosted`, `expires`). `method` carries the standard provenance vocabulary — broadcasts are always first-party from the organization (only `self_asserted` is valid today).",
        "required": [
          "id",
          "message",
          "datePosted",
          "expires",
          "organization",
          "location",
          "source",
          "method"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "message": {
            "type": "string",
            "minLength": 1,
            "maxLength": 280
          },
          "datePosted": {
            "type": "string",
            "format": "date-time"
          },
          "expires": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "expired",
              "retracted"
            ]
          },
          "method": {
            "type": "string",
            "enum": [
              "self_asserted"
            ],
            "description": "Standard provenance method (docs/provenance.md). Only `self_asserted` is valid today — broadcasts are always first-party from the organization. Field exists for symmetry across primitives; new methods can be added additively."
          },
          "organization": {
            "$ref": "#/components/schemas/Organization"
          },
          "location": {
            "$ref": "#/components/schemas/Place"
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          }
        }
      },
      "BroadcastInput": {
        "type": "object",
        "required": [
          "organizationId",
          "placeId",
          "message",
          "expires"
        ],
        "properties": {
          "organizationId": {
            "type": "string",
            "format": "uuid"
          },
          "placeId": {
            "type": "string",
            "format": "uuid"
          },
          "message": {
            "type": "string",
            "minLength": 1,
            "maxLength": 280
          },
          "expires": {
            "type": "string",
            "format": "date-time",
            "description": "Must be in the future and within 24h of now."
          }
        }
      },
      "Series": {
        "type": "object",
        "description": "A recurring activity with its own public identity (name, slug, description, cover image) separate from any individual instance. Completes the event_series primitive — recurrence machinery has existed; identity is the additive surface for consumers that want to address the series as a thing (subscribable entity in Merrie, series page in Fiber, etc.). Past instances' titles (`Event.name`) are NEVER rewritten when a series is renamed — historical accuracy. To list a series's instances, use `/events?series_id={id}`.",
        "required": [
          "id",
          "slug",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{0,99}$",
            "description": "Globally unique URL slug. Same format as organization and group slugs."
          },
          "name": {
            "type": "string",
            "description": "Current public identity of the series. Forward-looking — past instances retain their original titles."
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "cover_image_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "organizer": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Organization"
              },
              {
                "type": "null"
              }
            ],
            "description": "Organization running the series. Null only on legacy series whose original organizer was deleted."
          },
          "recurrence": {
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "rrule"
                ],
                "properties": {
                  "rrule": {
                    "type": "string",
                    "description": "RFC 5545 RRULE string."
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "next_instance": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Event"
              },
              {
                "type": "null"
              }
            ],
            "description": "Soonest upcoming instance of the series, or null if none are scheduled. For the full list of instances, query `/events?series_id={id}`."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SeriesIdentityInput": {
        "type": "object",
        "description": "Body for PATCH /service/series/{seriesId}. All fields optional; at least one required. Identity edits do NOT propagate to past or future instance titles — to rename future instances, use PATCH /service/events/series/{seriesId} with the same name change.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{0,99}$",
            "description": "If provided, must be unique. Server returns 409 on collision."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          },
          "cover_image_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "maxLength": 2000
          }
        }
      },
      "ListItem": {
        "type": "object",
        "description": "Schema.org ListItem. Used inside ItemList.itemListElement to pair an item reference with its position and an optional curator note.",
        "required": [
          "position",
          "item"
        ],
        "properties": {
          "position": {
            "type": "integer",
            "minimum": 1
          },
          "item": {
            "description": "The referenced entity. The shape matches the corresponding type's schema; consumers can discriminate on the schema or by looking at fields.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/Event"
              },
              {
                "$ref": "#/components/schemas/Organization"
              },
              {
                "$ref": "#/components/schemas/Place"
              }
            ]
          },
          "curatorNote": {
            "type": [
              "string",
              "null"
            ],
            "description": "Curator's optional commentary on this item."
          }
        }
      },
      "List": {
        "type": "object",
        "description": "Schema.org ItemList. A curatorial selection by an Organization — \"this weekend's picks,\" \"best park benches in Fishtown,\" etc. Curator is always an organization (the Person primitive is gone). `method` carries the standard provenance vocabulary — lists are always editorial assertions by the curator (only `self_asserted` is valid today).",
        "required": [
          "id",
          "slug",
          "name",
          "curator",
          "itemListElement",
          "method"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "method": {
            "type": "string",
            "enum": [
              "self_asserted"
            ],
            "description": "Standard provenance method (docs/provenance.md). Only `self_asserted` is valid today — lists are editorial assertions by the curator. Field exists for symmetry across primitives."
          },
          "curator": {
            "description": "The Organization that maintains this list.",
            "$ref": "#/components/schemas/Organization"
          },
          "itemListOrder": {
            "type": "string",
            "enum": [
              "Ascending",
              "Descending",
              "Unordered"
            ],
            "default": "Ascending"
          },
          "numberOfItems": {
            "type": "integer"
          },
          "itemListElement": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListItem"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ListInput": {
        "type": "object",
        "description": "Input for creating a list. Curator is always an organization — `curatorOrganizationId` is required.",
        "required": [
          "name",
          "curatorOrganizationId"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "slug": {
            "type": "string",
            "maxLength": 100
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "curatorOrganizationId": {
            "type": "string",
            "format": "uuid",
            "description": "Organization that curates this list. The calling service key must be linked to this org via `api_key_organization_links`."
          }
        }
      },
      "ListItemInput": {
        "type": "object",
        "required": [
          "position",
          "itemType",
          "itemId"
        ],
        "properties": {
          "position": {
            "type": "integer",
            "minimum": 1
          },
          "itemType": {
            "type": "string",
            "enum": [
              "event",
              "organization",
              "place"
            ]
          },
          "itemId": {
            "type": "string",
            "format": "uuid"
          },
          "curatorNote": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "VerificationPathRequest": {
        "type": "object",
        "description": "Only organizations verify; there is no targetType discriminator.",
        "required": [
          "organization_id",
          "identifier_type",
          "identifier_value"
        ],
        "properties": {
          "organization_id": {
            "type": "string",
            "format": "uuid"
          },
          "identifier_type": {
            "type": "string",
            "enum": [
              "email"
            ]
          },
          "identifier_value": {
            "type": "string"
          }
        }
      },
      "VerificationPathResponse": {
        "type": "object",
        "description": "Result of /service/verifications/path. The Commons dictates which submission endpoint to call next; apps follow.",
        "required": [
          "alreadyVerified"
        ],
        "properties": {
          "alreadyVerified": {
            "type": "boolean",
            "description": "True if this exact (target, identifier) pair is already verified."
          },
          "requiredMethod": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "domain_email_loop",
              "manual_review",
              null
            ]
          },
          "endpoint": {
            "type": [
              "string",
              "null"
            ],
            "description": "Path of the submission endpoint to call next."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Routing reason, e.g. 'business_email_domain', 'personal_email_domain', 'already_verified'."
          },
          "existingIdentifier": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/VerifiedIdentifier"
              },
              {
                "type": "null"
              }
            ],
            "description": "Populated when alreadyVerified=true."
          }
        }
      },
      "VerificationChallengeInput": {
        "type": "object",
        "description": "Only organizations verify; there is no targetType discriminator.",
        "required": [
          "organizationId",
          "identifierType",
          "identifierValue"
        ],
        "properties": {
          "organizationId": {
            "type": "string",
            "format": "uuid"
          },
          "identifierType": {
            "type": "string",
            "enum": [
              "email"
            ]
          },
          "identifierValue": {
            "type": "string",
            "format": "email"
          }
        }
      },
      "VerificationChallengeResponse": {
        "type": "object",
        "required": [
          "challengeId",
          "expiresAt"
        ],
        "properties": {
          "challengeId": {
            "type": "string",
            "format": "uuid"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VerificationConfirmInput": {
        "type": "object",
        "required": [
          "code"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "One-time code delivered via verification email."
          }
        }
      },
      "VerificationOutcome": {
        "type": "object",
        "description": "Result of a verification submission. Status `verified` means the row landed in organization_verifications; `pending` means manual review queued; `rejected` means the submission failed policy.",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "verified",
              "pending",
              "rejected"
            ]
          },
          "verifiedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "method": {
            "type": [
              "string",
              "null"
            ]
          },
          "identifier": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/VerifiedIdentifier"
              },
              {
                "type": "null"
              }
            ]
          },
          "reviewId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Set when status=pending; reference for tracking."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set when status=rejected."
          }
        }
      },
      "VerificationManualInput": {
        "type": "object",
        "description": "Submission for the manual-review path. Evidence is structured and required fields enforced at submit. Apps with verification_authority for the matching method auto-approve; others queue.",
        "required": [
          "organizationId",
          "identifierType",
          "identifierValue",
          "evidence"
        ],
        "properties": {
          "organizationId": {
            "type": "string",
            "format": "uuid"
          },
          "identifierType": {
            "type": "string",
            "enum": [
              "email"
            ]
          },
          "identifierValue": {
            "type": "string",
            "format": "email"
          },
          "evidence": {
            "type": "object",
            "required": [
              "phone",
              "verifiedVia",
              "reviewerAttestation",
              "reviewerAccountId",
              "businessAddressObserved",
              "idDocumentObserved"
            ],
            "properties": {
              "phone": {
                "type": "string"
              },
              "verifiedVia": {
                "type": "string",
                "enum": [
                  "in_person",
                  "video_call"
                ]
              },
              "reviewerAttestation": {
                "type": "string",
                "minLength": 1,
                "maxLength": 2000
              },
              "reviewerAccountId": {
                "type": "string",
                "description": "Identifier of the human reviewer who attests."
              },
              "businessAddressObserved": {
                "type": "boolean"
              },
              "idDocumentObserved": {
                "type": "boolean"
              },
              "supportingNotes": {
                "type": "string",
                "maxLength": 2000
              }
            }
          }
        }
      },
      "PendingReview": {
        "type": "object",
        "description": "An item in the verification review queue (admin-tier endpoints). Targets are always organizations.",
        "required": [
          "id",
          "organizationId",
          "identifierType",
          "identifierValue",
          "method",
          "submittedByApp",
          "evidence",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "organizationId": {
            "type": "string",
            "format": "uuid",
            "description": "The organization being verified."
          },
          "identifierType": {
            "type": "string"
          },
          "identifierValue": {
            "type": "string"
          },
          "method": {
            "type": "string"
          },
          "submittedByApp": {
            "type": "string"
          },
          "evidence": {
            "type": "object",
            "additionalProperties": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VerificationRejectInput": {
        "type": "object",
        "required": [
          "reason"
        ],
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "INSUFFICIENT_EVIDENCE",
              "IDENTIFIER_DISPUTED",
              "IMPOSTER_SIGNALS",
              "OUT_OF_POLICY"
            ]
          },
          "note": {
            "type": "string",
            "maxLength": 1000
          }
        }
      },
      "DisputeInput": {
        "type": "object",
        "description": "Records a dispute against a verified target, identifier, or other data record. v2 stores for operator review only — no automated action. The `person` target type was dropped in v2.",
        "required": [
          "targetType",
          "targetId",
          "reason"
        ],
        "properties": {
          "targetType": {
            "type": "string",
            "enum": [
              "organization",
              "verified_identifier"
            ]
          },
          "targetId": {
            "type": "string",
            "format": "uuid"
          },
          "reason": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          },
          "submitterContact": {
            "type": "string",
            "description": "Optional email or phone for follow-up."
          }
        }
      },
      "DisputeResponse": {
        "type": "object",
        "required": [
          "id",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "recorded"
            ]
          }
        }
      }
    },
    "responses": {
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Request payload or query parameters failed validation",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "browseApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Optional for read endpoints (higher rate limit bucket). Required for webhook management."
      },
      "serviceApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Service-tier API key. Scoped: can only modify data for accounts linked to this key. Admin keys bypass scoping."
      }
    }
  }
}
