# Neighborhood Commons — The Complete Guide > Open neighborhood public-facts infrastructure. Five typed atoms — places, organizations, events, broadcasts, lists — that any app can read, mix, and remix. Schema.org-aligned. CC BY 4.0. Read free, no key required. This document is the AI-readable mirror of the homepage at `https://neighborhood-commons.org`. Everything a developer or LLM agent needs to integrate is here, in one place, in document order. No prior knowledge assumed; no other documents required. Base URL: `https://neighborhood-commons.org` **This document is the Guide, part of the Commons Contract.** The Contract is three files: - **The Spec** — [`/openapi.json`](https://neighborhood-commons.org/openapi.json) — machine-readable, authoritative. Generate your client from it. - **The Guide** — this file. Narrative companion. Explains *why* and *how*. - **The Log** — [`CHANGELOG.md`](https://github.com/joinfiber/neighborhood-commons/blob/master/CHANGELOG.md) — dated record of every change. **Spec wins. Guide explains. Log dates.** If something here contradicts the Spec on a matter of fact, trust the Spec and open an issue. --- ## What the Commons Is The Neighborhood Commons is a public store of public facts about neighborhoods. That's the whole job. Five typed atoms compose to express most of what a neighborhood produces as public information: - **Place** — physical locations (a venue, a park, an address) - **Organization** — entities that publish or organize (businesses, community groups, nonprofits, collectives, solo operators) - **Event** — activities at specific times, organized by an Organization, at a Place - **Broadcast** — ephemeral signals from an Organization, pinned to a Place, max 24h lifetime - **List** — editorial selections of events, organizations, or places, curated by an Organization Plus a narrow verification system that anchors first-party authority for an organization's profile data. Each type maps to a Schema.org concept. Apps consume by composing filters that match their editorial — there is no default firehose. Apps contribute by integrating at the service tier, which carries real onboarding and accountability. The 3.0 surface lands the five primitives above. Future minor versions add additional public-fact types as consumer apps need them — classifieds, civic notices, plans, assets (the wider Craigslist-shaped frontier). The substrate scales by vocabulary, not by complexity at any single point. ### Two kinds of public facts The Commons holds two distinguishable kinds of facts. The distinction is load-bearing — it drives the verification model, the publishing model, and the consumer-side filtering posture. **Type A — Durable profile data.** What an entity *is*. Name, address, hours, contact, logo, description, accessibility. Asserted only by the verified first party. Until verification, the Commons may hold claimed/seeded data (from ingestion pipelines, scrapes) but flags it as unverified. Errors persist indefinitely as infrastructure misinformation — high stakes. **Type B — Transactional/episodic data.** What an entity *does*. Events, broadcasts, list memberships. Publisher must have structural authority over their specific contribution. Errors are bounded — the event passes, the broadcast expires. Verification machinery scopes narrowly to Type A. That's what makes it load-bearing rather than optional or overengineered. ### Three pillars **01 — Open.** Every read endpoint is open. No API key required for reads. The 3.0 spec is committed to additive-only stability — what works today works in 18 months. **02 — Composable.** The Commons isn't a destination. Each app composes its own slice through opt-in filters: by contributor, by proximity, by category, by tag. Same data, different editorial. **03 — Authoritative.** Verification anchors first-party authority for an organization's profile data. Apps reading the Commons see whether an organization has been verified, with what method, and when. The rest is honestly attributed — every contribution carries the publisher's identity, the method, and the license, so consumer apps can decide who to trust. ### Design principles The Commons is **thin**. It stores typed atoms and serves them. Curation, social features, recommendations, user accounts, ingestion pipelines — all of that belongs in the apps that build on top. The Commons is **durable**. From 3.0 forward, the spec is **additive-only** within a major version. Future versions add types/fields/endpoints without breaking existing consumers. Removals or renames require a 4.0+ release and very strong justification — measured in years, not months. The explicit taxonomy of what counts as breaking vs. additive lives in [`docs/stability-promise.md`](https://github.com/joinfiber/neighborhood-commons/blob/master/docs/stability-promise.md). The Commons is **authoritative on facts**. Every response is self-contained: typed Schema.org-aligned shapes, no implicit knowledge, no extra joins to interpret a record. The Commons is **opt-in, not firehose**. Read endpoints accept filter parameters that compose into each app's editorial slice. Verification is a *signal* apps consume, not a permission gate the Commons enforces. The Commons holds **no user accounts**. Apps that have users own their users. Personal identifiers — emails, names, phones — are between users and the apps they sign up for. Entities (businesses, groups, places) are public; the people behind them are private. --- ## 1. Quick Start You can read every endpoint without authentication. Start here. ### Read events near a point ```bash curl "https://neighborhood-commons.org/api/v1/events?near=39.97,-75.14&radius_km=2&limit=5" ``` Returns up to five published events within 2 km of the supplied coordinates. Response shape: `{ meta, events }`. Every event is self-contained — no further joins required to render it. ### Filter by verification status ```bash curl "https://neighborhood-commons.org/api/v1/organizations?verified=true" ``` Returns organizations that have been verified. Add `near=lat,lng&radius_km=5` to scope geographically. ### Filter events by authority tier ```bash curl "https://neighborhood-commons.org/api/v1/events?first_party=true" ``` Returns only events posted by the verified organization itself — the authority tier. Filter to `first_party=false` for events aggregated from public sources (scrapers, feeds, witnessed-with-evidence contributions). ### Read what's broadcasting right now ```bash curl "https://neighborhood-commons.org/api/v1/broadcasts?near=39.97,-75.14&radius_km=1" ``` Active broadcasts only — expired and retracted broadcasts are filtered server-side. Response includes the embedded `organization` and `location` for each broadcast. ### Calendar feed ``` https://neighborhood-commons.org/api/v1/events.ics ``` Standards-compliant iCalendar. Drop the URL into Google Calendar / Apple Calendar / any RFC 5545 reader. RSS available at `/api/v1/events.rss`. **Writing data?** Skip to [Section 4: Writing data](#4-writing-data). You can self-issue a service key in `pending` status and build your full integration immediately — only the live-write step requires a one-time review. --- ## 2. Types The 1.0 substrate is five typed atoms. Each maps to a Schema.org concept. Every response shape is self-contained — no implicit knowledge, no extra joins to interpret a record. Field names follow Schema.org canonical (camelCase) on output; database columns underlying them are snake_case. | Type | Schema.org | What it represents | |---|---|---| | `Place` | Place | Physical location — venue, park, address. | | `Organization` | Organization · LocalBusiness | The unified entity primitive — business, community group, nonprofit, collective, solo operator. | | `Event` | Event | Activity at a specific time, organized by an Organization, at a Place. | | `Broadcast` | (novel) | Ephemeral signal from an Organization, pinned to a Place, max 24h lifetime. | | `List` | ItemList | Editorial selection of events, organizations, or places. | ### 2.1 Place Schema.org `Place`. Physical locations, deduplicated by Google Places ID where available. The canonical record for an address; events and organizations link to it by `id`. | Field | Type | Notes | |---|---|---| | `id` | uuid | Internal Place ID. Used as FK from Events, Organizations, etc. | | `name` | string | Canonical name. | | `address` | PostalAddress | Schema.org `PostalAddress`: `streetAddress`, `addressLocality`, `addressRegion`, `postalCode`, `addressCountry`. | | `geo` | GeoCoordinates | `{ latitude, longitude }`. Always present. | | `identifier` | PropertyValue[] | External IDs. Common: `{ propertyID: "googlePlaceId", value: "ChIJ..." }`. | | `placeCategories` | string[] | Categorical labels sourced from OpenStreetMap (ODbL). Empty array when OSM has no categorization. | | `categorySource` | string | Provenance of `placeCategories`: `osm`, `admin_review`, or `publisher_declaration`. | ```json { "id": "f1b2c3d4-...", "name": "Espresso Co", "address": { "streetAddress": "1234 Main St", "addressLocality": "Philadelphia", "addressRegion": "PA", "postalCode": "19125", "addressCountry": "US" }, "geo": { "latitude": 39.97, "longitude": -75.13 }, "identifier": [ { "propertyID": "googlePlaceId", "value": "ChIJ..." } ], "placeCategories": ["cafe", "coffee_shop"], "categorySource": "osm" } ``` > **Place categorization comes from OpenStreetMap.** The Commons stores `googlePlaceId` (Google's terms permit indefinite storage of place IDs) but does not persist Google's other response data. Categorization is sourced from OSM under ODbL licensing, with attribution acknowledged. Where OSM has gaps, an operator can supplement via admin review or a verified publisher can refine during their own profile setup. ### 2.2 Organization Schema.org `Organization`. The unified entity primitive — anything that publishes or organizes is an organization, regardless of how many humans operate it. A touring DJ is an organization-of-one; a chess club is an organization-of-many. Same primitive. | Field | Type | Notes | |---|---|---| | `id` | uuid | Internal Organization ID. | | `slug` | string | URL-safe identifier. Unique. | | `name` | string | Public-facing name. | | `legalName` | string \| null | Optional registered legal name (for businesses, nonprofits). | | `description` | string \| null | | | `url` | string \| null | | | `logo` | string \| null | URL to logo image. | | `image` | string \| null | URL to hero / representative image. | | `telephone` | string \| null | Public-facing contact phone. | | `email` | string \| null | Public-facing contact email. | | `sameAs` | string[] | Canonical URLs (Wikipedia, Wikidata, social profiles). | | `keywords` | string[] | Searchable terms. | | `tags` | string[] | Descriptive labels. Free-form within rules; a recommended starter vocabulary is published at `/v1/meta/tags`. Used by consumer apps for filtering and display. | | `commercial` | boolean \| null | True for for-profit; false for non-profit/community-oriented; null for unspecified. | | `openingHoursSpecification` | object[] | Schema.org `OpeningHoursSpecification` array. | | `location` | Place \| null | Primary place reference, fully hydrated. | | `verified` | boolean | True if at least one active verification record exists for this org. | | `verification` | object \| null | If verified: `{ method, verifiedAt, verifiedByApp }`. | ```json { "id": "...", "slug": "bubs", "name": "Bubs", "description": "Community board game shop", "url": "https://bubs.example", "logo": "https://...", "telephone": "+12155550100", "email": "hi@bubs.example", "sameAs": ["https://instagram.com/bubs"], "keywords": ["board games", "community"], "tags": ["retail", "games", "all-ages"], "commercial": true, "openingHoursSpecification": [ { "dayOfWeek": "Monday", "opens": "12:00", "closes": "22:00" } ], "location": { "id": "...", "name": "Bubs", "address": {...}, "geo": {...} }, "verified": true, "verification": { "method": "domain_email_loop", "verifiedAt": "...", "verifiedByApp": "Holler" } } ``` An Organization may have a single `location` (the primary place) and additional places via the `organization_places` link table — chains, pop-ups, shared spaces. > **No `kind` discriminator.** Earlier versions of the spec had a `kind` enum (local_business, community_group, curator, etc.). v2 removed it because the values mixed structural facts with vibes with legal status, and forced false choices that narrowed reach. Use `tags` for descriptive classification and `commercial` for the for-profit/non-profit signal. Consumer apps derive richer classification from structural signals: the org's place (with OSM categories), the events it has posted over time, its tags, its description text. ### 2.3 Event Schema.org `Event`. Activity at a specific time, at a Place, organized by an Organization. Events are the most-used type in the substrate; the table below covers the core. The full schema is in `/openapi.json`. | Field | Notes | |---|---| | `id`, `name`, `description` | Standard. | | `start`, `end`, `timezone` | ISO 8601 with timezone offset. Field name is `start` (not `startDate`) per the upstream Neighborhood API spec. | | `location` | Embedded Place reference. | | `organizer` | Embedded Organization reference. Always an organization in v2 (no Person variant). | | `performer` | Array of performers, each a `{ organization?, performerName?, role? }`. Free-form `performerName` allowed for performers without a Commons organization. | | `category`, `tags` | Categorization. See [Appendix A: Categories](#appendix-a-categories) and [Appendix B: Tags](#appendix-b-tags). | | `cost`, `url`, `images` | Practical fields. | | `recurrence`, `series_id`, `series_instance_number`, `series_instance_count` | RRULE-based recurrence; series link. | | `open_window`, `capacity`, `rsvp`, `wheelchair_accessible` | Visibility and access metadata. | | `first_party` | Boolean. True iff the organizer had an active verification at the time of writing — the authority signal. Server-computed; not a caller input. | | `source` | Provenance under the four-role frame: `{ method, url, contributor, collected_at, license }`. `method` is one of `self_asserted`, `proxied`, `witnessed`. `url` is non-null only for `proxied`. The "who is this from?" role is filled by `organizer.name`, not by a `source.publisher` field. See [`docs/four-roles.md`](docs/four-roles.md). | #### Timestamps and timezones Event times use ISO 8601 with timezone offset. The offset is DST-aware and computed for the event's actual date — events in the same venue across DST boundaries will have different offsets. ``` "start": "2026-04-15T20:00:00-04:00", // DST active "start": "2026-12-15T20:00:00-05:00", // DST inactive ``` The `timezone` field is the IANA name (`America/New_York`) and is authoritative for DST rules. Treat the offset on `start`/`end` as correct-at-time-of-storage; if you re-render in a different timezone, use `timezone` to re-resolve. #### Recurring events Recurrence uses RRULE format (RFC 5545). Bounded series (`COUNT=N` or `UNTIL=...`) materialize that many instances. Unbounded series materialize a rolling 6-week horizon and extend automatically via daily cron. | Pattern | RRULE | |---|---| | Every day | `FREQ=DAILY` | | Every week | `FREQ=WEEKLY` | | Every 2 weeks | `FREQ=WEEKLY;INTERVAL=2` | | Every month | `FREQ=MONTHLY` | | 2nd Friday of each month | `FREQ=MONTHLY;BYDAY=2FR` | | Mon/Wed/Fri | `FREQ=WEEKLY;BYDAY=MO,WE,FR` | | 12 weeks then stop | `FREQ=WEEKLY;COUNT=12` | `series_instance_count` on each event tells you the total instances in the series — useful for showing "Week 3 of 12" UI without a second call. `collapse_series=true` on `GET /v1/events` deduplicates a series to its next upcoming instance — useful for "tonight near me" feeds. ### 2.4 Broadcast Ephemeral signal from an Organization, pinned to a Place. Maximum 24h lifetime. No direct Schema.org analog; `SpecialAnnouncement` was checked and rejected as a fit for ephemeral commercial signals — conventions for `datePosted` and `expires` borrowed from there. ```json { "id": "...", "message": "Half off all sandwiches until we sell out", "datePosted": "2026-05-05T14:00:00-04:00", "expires": "2026-05-05T18:00:00-04:00", "status": "active", "organization": { "id": "...", "name": "Bubs", "verified": true }, "location": { "id": "...", "name": "Bubs", "geo": {...} }, "method": "self_asserted", "source": { "method": "self_asserted", "url": null, "contributor": { "name": "Holler", "url": null }, "collected_at": "..." } } ``` Broadcast creation is not gated on the organization being verified — apps decide whether to surface unverified broadcasts in their feeds. Use `verified=true` filters on `GET /v1/broadcasts` to scope editorially. ### 2.5 List Schema.org `ItemList`. Editorial selections by an Organization. Items are polymorphic — a single list can hold events, organizations, places, or any mix. Items reference existing primitives; lists do not create primitives. ```json { "id": "...", "slug": "this-weekends-picks", "name": "This Weekend's Picks", "description": "Five things worth your Saturday", "curator": { "@type": "Organization", "id": "...", "name": "Olive's Picks", "tags": ["curator"] }, "itemListOrder": "Ascending", "numberOfItems": 5, "itemListElement": [ { "@type": "ListItem", "position": 1, "item": { "@type": "Event", "id": "...", "name": "..." }, "curatorNote": "If you only see one thing, see this." } ] } ``` Lists are how curator workflows surface in the substrate. A curator-organization builds a list; the items are references to events, organizations, or places that already exist in the Commons. Lists are overlays, not a publishing path — see Section 4 for the constraints. ### 2.6 ContributorProfile (3.1) The public-facing identity of each contributing app — the splash card data a consumer app like Fiber renders when a reader taps "via Merrie" on an event. Distinct from the organization that *organized* the event; this is the *app that routed it into the Commons* (the four-role frame's contributor slot — see `docs/four-roles.md`). ```json { "id": "...", "slug": "merrie", "name": "Merrie", "tagline": "Publish what your group is up to", "description": "Merrie is a publishing tool for community groups.", "who_its_for": "Community organizers", "app_url": "https://merrie.co", "logo_url": "https://r2.example/merrie-logo.png", "category": "publishing", "created_at": "...", "updated_at": "..." } ``` The slug is the stable identifier — survives api_key rotation. Profiles are registered through the developer dashboard at `/developers` (ships incrementally in subsequent releases). Only `status = 'active'` profiles surface on `GET /v1/contributors`; pending/suspended live on the operational side. When an event is published via a registered contributor's key, the event's `source.contributor` is enriched with `{slug, logo_url, description, profile_url}` in addition to the existing `{name, url}`. Pre-3.1 events fall back to the legacy `{name, url}` snapshot fields with the new fields as null. The expansion is additive — consumers that read only `name`/`url` continue to work unchanged. ### 2.7 Series A recurring activity with its own public identity, separate from any individual instance. Used by consumer apps that want to address the series as a thing — a subscribable Quizzo show, a dedicated series page, a "weekly trivia" entity that doesn't collapse to one instance. The recurrence machinery has existed since v1; v1.x adds identity so the series is *addressable* across the ecosystem rather than reconstructed from instance templates. ```json { "id": "...", "slug": "fishtown-quizzo", "name": "Fishtown Quizzo", "description": "Weekly drop-in trivia at Frankford Hall.", "cover_image_url": "https://r2.example/fishtown-quizzo-cover.jpg", "organizer": { "@type": "Organization", "id": "...", "name": "Quizzo Philly" }, "recurrence": { "rrule": "FREQ=WEEKLY;BYDAY=TU" }, "next_instance": { "@type": "Event", "id": "...", "name": "Fishtown Quizzo", "start": "..." }, "created_at": "...", "updated_at": "..." } ``` Two reads: - `GET /v1/series/:idOrSlug` for the series itself. - `GET /v1/events?series_id={id}&from=...` for the materialized instances under it. Don't conflate. The series carries identity; the events carry instance-time facts. They share `series_id` but model different things. **Past instances are never renamed.** Renaming a series is forward-only — `event_series.name` becomes the new identity, but past `Event.name` values stay as they were when each instance happened. Historical accuracy of "what was happening that day" matters more than uniform branding across time. **Two PATCH endpoints, different semantics.** `PATCH /service/series/{id}` edits the series identity (name, slug, description, cover) — fires `series.updated`, touches no events. `PATCH /service/events/series/{seriesId}` edits the per-instance template — propagates to future instance rows. To rename both the series and the future instances, call both. **Cover images** flow through `POST /service/series/{seriesId}/cover` (base64 JSON, magic-byte validated, Sharp re-encoded, stored on Commons' R2). Use this rather than uploading covers through a per-app pipeline — every consumer renders the same URL pattern, so cross-product image rendering stays consistent. The endpoint persists the URL to `cover_image_url` and fires `series.updated` in one shot. A consumer-app design principle worth restating here: **publishers declare; readers classify.** The Commons holds publisher declarations like `events.rsvp` (`recommended` / `required`) — these travel with the data. It does not hold reader-side taxonomies like `attendance_model: drop_in | rsvp_per_instance`, which classify events into UX buckets. Apps derive their own classifications from the publisher declarations on each instance. --- ## 3. Reading data All read endpoints are unauthenticated. Optional `X-API-Key` header bumps rate limits; otherwise requests are limited per IP. ### 3.1 Endpoints | Method | Path | Returns | |---|---|---| | `GET` | `/v1/events` | Paginated event list. | | `GET` | `/v1/events/:id` | Single event by ID. | | `GET` | `/v1/events.ics` | iCalendar feed. | | `GET` | `/v1/events.rss` | RSS 2.0 feed. | | `GET` | `/api/events/changes?since=...` | Lightweight sync feed (events updated since a cursor). Lives outside `/v1` — full URL `https://neighborhood-commons.org/api/events/changes`. | | `GET` | `/v1/places` | Paginated place list. | | `GET` | `/v1/places/:id` | Single place. | | `GET` | `/v1/organizations` | Paginated organization list. | | `GET` | `/v1/organizations/:idOrSlug` | Single organization by id or slug. | | `GET` | `/v1/publishers` | Paginated organization list, filtered to entities that publish (have at least one event or broadcast). | | `GET` | `/v1/publishers/:idOrSlug` | Single publisher record. | | `GET` | `/v1/broadcasts` | Active broadcasts only. | | `GET` | `/v1/broadcasts/:id` | Single broadcast. | | `GET` | `/v1/lists` | Paginated list of lists. | | `GET` | `/v1/lists/:idOrSlug` | Single list with hydrated items. | | `GET` | `/v1/contributors` | Paginated list of active contributor profiles (the public-facing identity of each app routing data into the Commons). | | `GET` | `/v1/contributors/:idOrSlug` | Single contributor profile. The "via X" splash card data, fetched by slug. | | `GET` | `/v1/series` | Paginated series list (recurring activity with its own identity). Filter by `organizer_org_id`. | | `GET` | `/v1/series/:idOrSlug` | Single series with organizer, recurrence, and next upcoming instance. | | `GET` | `/v1/meta` | Spec metadata, regions, categories, stats. | | `GET` | `/v1/meta/regions` | Active regions. | | `GET` | `/v1/meta/categories` | Event category vocabulary. | | `GET` | `/v1/meta/tags` | Recommended organization tag vocabulary. | | `GET` | `/v1/meta/stats` | Aggregate counts. | ### 3.2 Filter composition Filters apply at the URL query level. Compose them freely; all are optional. | Filter | Applies to | Effect | |---|---|---| | `first_party=true` | events | Only events posted by the verified organization itself — the authority tier. | | `first_party=false` | events | Only events aggregated from public sources (scrapers, feeds, witnessed-with-evidence). | | `verified=true` | organizations, publishers | Only verified organizations. | | `verified=false` | organizations, publishers | Only unverified organizations. | | `tag=slug` | organizations, publishers, events | One or more tag matches. Multiple tags use AND semantics. | | `commercial=true|false` | organizations, publishers | Filter by for-profit / non-profit signal. | | `place_category=slug` | organizations, publishers, places | Filter by OSM-sourced place categorization (e.g., `cafe`, `live_music_venue`). | | `created_by_contributor=slug` | events, organizations, publishers, broadcasts | Filter to records this app contributed — the `source.contributor` axis. Events, organizations, and publishers resolve the registered `contributor_profile` slug (only `active` profiles match); broadcasts currently match the contributor name. | | `near=lat,lng` | events, places, organizations, broadcasts | Coordinates as `"39.97,-75.13"`. Pair with `radius_km`. | | `radius_km=N` | events, places, organizations, broadcasts | Proximity radius. Required when using `near`. | | `q=text` | events, places, organizations, lists | Text search across name and description. | | `category=slug` | events | Single category filter. See Appendix A. | | `start_after`, `start_before` | events | ISO 8601 date or datetime. | | `collapse_series=true` | events | Deduplicates recurring events to the next upcoming instance. | | `series_id=uuid` | events | Filter to events in a specific series. | Filter composition is the editorial primitive. App A surfaces only first-party events; App B surfaces everything but visually differentiates by authority tier; both express that as URL parameters. There is no default firehose — apps construct their own slice. The Commons holds two authority tiers on the same data substrate: - **Public facts (`first_party=false`).** Information *about* an organization or event — scraped from a venue's website, ingested from a partner feed, witnessed via Fiber-Community OCR of a public flyer. Source-tracked, possibly accurate, best-effort quality. Today this is the dominant tier. - **First-party (`first_party=true`).** Information *from* the organization — events the verified organization posted themselves, broadcasts they issued, profile data they curated. Available only after the organization verifies; the organization stands behind every record. Apps choose: maximum coverage (take everything), maximum authority (filter to `first_party=true`), or composed (surface both with visual differentiation). ### 3.3 Pagination & rate limits List endpoints accept `limit` (1–200, default 50) and `offset` (default 0). Response shape: ```json { "meta": { "total": 142, "limit": 50, "offset": 0, "spec": "neighborhood-api-v0.2", "license": "CC-BY-4.0" }, "events": [ ... ] } ``` Rate limit: **1,000 requests/hour** per IP — or per API key when an `X-API-Key` header is sent. Limits are returned in standard `RateLimit-*` headers. `429` indicates the current window is exhausted; back off and retry after the window rolls over. Cache hints: read responses include `Cache-Control: public, max-age=60` for events, organizations, places, and lists. Broadcasts use `max-age=15` because they're ephemeral. Honor these on the consumer side to keep both your costs and Commons load down. ### 3.4 Visibility rules Published events appear in feeds according to these rules: 1. **`status = 'published'`** — drafts, `pending_review`, and `unpublished` events are excluded from public reads. 2. **Organization is not suspended** — events from suspended organizations return `404`, not `403`. The Commons does not disclose whether a suspended organization exists. 3. **Time gate** — controlled by `open_window`: - `open_window = false` (default): visible until `start`, hidden after. - `open_window = true`: visible until `end`, or `start + 3 hours` if no end time. 4. **Region filter** — when a consumer provides coordinates (`near=lat,lng` + `radius_km`), results are filtered to events whose resolved `region_id` matches the provided location. ### 3.5 Keeping in sync For consumers polling regularly, use `/api/events/changes?since=` to pull only events updated since a cursor. Returns updated events plus IDs of events that have ended. Cached 60s, rate limited 10/min per IP. For higher throughput, use a service key or webhooks. --- ## 4. Writing data Writes go through a service key. You can self-issue a key and build your entire integration before talking to anyone — auth, request shapes, verification flows, webhook signatures, every endpoint. The only thing gated is actually delivering data to the Commons. Keys ship in `pending` status and stay there until a one-time review activates writes. Reads are unrestricted either way. ### 4.0 Constrained publishing — three authority paths The Commons accepts writes from publishers with authority over what they publish. Three valid authority shapes: 1. **Entity-runs-it.** A verified organization publishing about itself. The service key writing the record must be linked to that organization via `api_key_organization_links`. This is how a venue posts its own shows, a chess club posts its own meetings, a salon manages its own profile. 2. **Pipeline-proxies-an-authoritative-source.** An ingestion pipeline (typically Studio) writes records lifted from an authoritative external source — a venue's website calendar, a partner's structured feed, an imported spreadsheet. The pipeline is a faithful proxy for a known source; `source.contributor` honestly attributes the original source. A key with `proxy_authority=true` sets `source_method='proxied'` and supplies the public `source_feed_url` it extracted from; the organizer stays the scraped real-world entity. Like the witnessed path, this bypasses the `api_key_organization_links` check (the pipeline doesn't own the venue). 3. **Witnessed-with-evidence.** A service key with `witness_authority=true` writes records on behalf of a collective identity, with evidence attached. The Fiber Community OCR contribution path is the canonical example: a Fiber user photographs a public flyer; the event enters the Commons attributed to "Fiber Community" with the photo as documentary evidence. Individual users are never identified; the publisher is the collective. What's explicitly excluded: a wild-west path where any party writes events they have no authority over. The constrained model means most events come from one authoritative publisher; conflict cases (a venue later posting an event a witness reported earlier) are rare and handled by the authority hierarchy. ### 4.0.1 Writing isn't reading Understand that **write access does not get you read.** The Commons is a substrate, not a megaphone. Every consumer app that reads from `/v1/*` makes its own filtering decisions: - **Publisher allow-list / block-list** — apps surface events from publishers they recognize and trust. - **`?first_party=true`** — apps return only events from verified organizations. - **`?source_method=self_asserted`** vs `?source_method=proxied` vs `?source_method=witnessed` — different consumers trust different authority paths (see [`docs/four-roles.md`](docs/four-roles.md)). - **Contributor recognition** — `source.contributor` carries per-event attribution. Apps may surface events from contributors they trust. - **Soft signals in UI** — even apps that fetch everything may only *display* publishers they recognize. What this means for you as a contributor: - **Quality drives uptake; volume doesn't.** A small, accurate dataset travels farther than a large sloppy one. - **Pursue verification.** First-party events get surfaced by apps filtering on `first_party=true`. Verification is the single most important quality signal in the ecosystem. - **Attribute honestly.** `source.contributor` lets you credit the actual originator. Apps trust contributors who attribute correctly. - **Your `business_name` is your brand.** Treat your publisher identity like one. If you internalize one thing from this section: **the Commons gives you write access; it doesn't give you readership.** Readership is earned by publishing data other apps want to surface. ### 4.0.2 Read-first, write-later adoption The most common adoption pattern: an app starts as a reader (no key needed), later adds webhooks (developer-tier key, self-served), and only registers a service-tier key when it has something to contribute. | Tier | Activated via | Reads | Writes | Webhook subscriptions | |------|---------------|-------|--------|-----------------------| | (none) | n/a | per-IP rate limit | no | no | | Developer | operator-issued by request (no self-serve flow yet) | dedicated rate limit | no | yes | | Service (pending) | `/developers/sign-up` portal flow | service-tier rate limit | `403 KEY_PENDING` | yes | | Service (active) | operator review at `/operator/applications` | service-tier rate limit | yes (all `/service/*` endpoints) | yes | Most developers go straight to service-tier — the portal sign-up is self-serve and reads work immediately. Developer-tier exists for the small set of consumers that want webhook subscriptions but never write events; if that's you, email the operator. ### 4.1 Get a key Sign up via the developer portal at [https://neighborhood-commons.org/developers/sign-up](https://neighborhood-commons.org/developers/sign-up). The flow is a guided form: email, app name, what you're building, how publishers in your flow have authority over what they publish. You'll verify via a one-time email code, and the dashboard lands you on your service key — copy it once, it's not shown again. Use it as `X-API-Key: nc_...` on every subsequent request. The key lands with `status: "pending_activation"` — reads work immediately; writes return `403 KEY_PENDING` until an operator reviews and activates. (The legacy `POST /v1/service/register/*` endpoints now return `410 ENDPOINT_RETIRED` pointing at the portal. Existing keys keep working — only the registration path moved.) ### 4.2 Activate writes The operator reviews your application via `/operator/applications`, looking at the same fields you submitted: what you're building, how you verify publishers. Approval is one click and flips a single column on your key — no rotation, no code changes on your side. You'll get an activation email when it happens, usually within a business day. If you're a witnessing app (OCR of public flyers, user-witnessed evidence), the operator can additionally provision a collective Organization at approval time; the email includes its UUID and a usage example. After activation, returning logins go through `/developers/login` — magic link to your registered email, lands you back on the dashboard. The dashboard requires MFA enrollment before operator access; standard developer use doesn't. ### 4.3 Scoping rules - Reads are unrestricted. Active service keys can read any public data; pending keys can too. - Writes are scoped: an active service key can only create/update/delete data for organizations linked to it via `api_key_organization_links`. Cross-organization writes return `403 NOT_LINKED`. - Admin keys (`is_admin=true`, currently used only by Studio) bypass scoping. - Witness-authority keys (`witness_authority=true`) can publish events with `source_method='witnessed'` attributed to their app-collective organization. Granted at activation for specific use cases (e.g., Fiber's OCR contribution). - Proxy-authority keys (`proxy_authority=true`) can publish events with `source_method='proxied'`, attributed to the scraped real-world organizer and carrying the public `source_feed_url` the data was lifted from (required for `proxied`). Bypasses the org-link check like the witnessed path. Granted at activation for trusted scrape-and-publish pipelines (e.g., Studio's porchfest importer). See [`docs/four-roles.md`](docs/four-roles.md) Path 2. ### 4.4 Places — register a venue Idempotent on `googlePlaceId`. If a Place with the supplied Google Place ID already exists, the response is `200` with the existing record. Otherwise `201` with the new one. ```bash curl -X POST "https://neighborhood-commons.org/api/v1/service/places" \ -H "X-API-Key: $NC_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Bubs", "googlePlaceId": "ChIJ...", "address": { "streetAddress": "1234 Main St", "addressLocality": "Philadelphia", "addressRegion": "PA", "postalCode": "19125" }, "geo": { "latitude": 39.97, "longitude": -75.13 } }' ``` Place categorization (`placeCategories`) is populated server-side from OpenStreetMap during ingestion. The Commons does not accept Google response data for storage beyond the `googlePlaceId` itself. ### 4.5 Organizations — create, link, update Three relevant endpoints: create a new Organization (auto-links the calling key), link your key to an existing Organization, or update an Organization you're already linked to. ```bash curl -X POST "https://neighborhood-commons.org/api/v1/service/organizations" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "name": "Bubs", "description": "Community board game shop", "url": "https://bubs.example", "telephone": "+12155550100", "email": "hi@bubs.example", "tags": ["retail", "games", "all-ages"], "commercial": true, "primaryPlaceId": "PLACE_UUID" }' # Link to an existing Organization (if another app created it first) curl -X POST "https://neighborhood-commons.org/api/v1/service/organizations/link" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "organizationId": "ORG_UUID" }' # Update fields on an Organization linked to this key curl -X PATCH "https://neighborhood-commons.org/api/v1/service/organizations/ORG_UUID" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "description": "Updated bio.", "telephone": "+12155550101" }' ``` Image uploads (logo, hero image) are JSON-base64: `POST /v1/service/organizations/:id/logo` and `POST /v1/service/organizations/:id/image` with body `{ "image": "" }`. Magic-byte check, Sharp re-encoding, R2 upload. ### 4.6 Events — using the organizer model Event creation requires `organizerOrganizationId`. The calling service key must be linked to that organization, OR have `witness_authority=true` with `source_method='witnessed'`, OR have `proxy_authority=true` with `source_method='proxied'` (which also requires `source_feed_url`). ```bash curl -X POST "https://neighborhood-commons.org/api/v1/service/events" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "organizerOrganizationId": "ORG_UUID", "name": "Thursday Jazz Night", "start": "2026-05-07T19:00:00-04:00", "end": "2026-05-07T22:00:00-04:00", "timezone": "America/New_York", "category": "live-music", "location": { "name": "Bubs", "address": "1234 Main St, Philadelphia PA", "place_id": "ChIJ..." }, "description": "Weekly jazz trio. BYOB.", "cost": "Free" }' # Reassign an event's organizer curl -X PATCH "https://neighborhood-commons.org/api/v1/service/events/EVENT_UUID/organizer" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "organizerOrganizationId": "ORG_UUID" }' ``` ### 4.7 Broadcasts — ephemeral signals Maximum 24h lifetime. Body: `organizationId`, `placeId`, `message` (≤280 chars), `expires` (ISO 8601, must be future, ≤24h ahead). ```bash curl -X POST "https://neighborhood-commons.org/api/v1/service/broadcasts" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "organizationId": "ORG_UUID", "placeId": "PLACE_UUID", "message": "Half off sandwiches until we sell out", "expires": "2026-05-05T18:00:00-04:00" }' # Retract early curl -X POST "https://neighborhood-commons.org/api/v1/service/broadcasts/BROADCAST_UUID/retract" \ -H "X-API-Key: $NC_KEY" ``` Verification gate: broadcast creation does *not* require the organization to be verified. Verification is a read-time consumer-app filter. ### 4.8 Lists — editorial overlays Lists are editorial overlays over existing primitives. List items can only reference event/organization/place records that already exist; lists do not create new primitives. ```bash # Create the list (curator is an Organization) curl -X POST "https://neighborhood-commons.org/api/v1/service/lists" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "name": "This Weekend'\''s Picks", "description": "Five things worth your Saturday", "curatorOrganizationId": "ORG_UUID" }' # Add an existing event to the list by position curl -X POST "https://neighborhood-commons.org/api/v1/service/lists/LIST_UUID/items" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "position": 1, "itemType": "event", "itemId": "EVENT_UUID", "curatorNote": "If you only see one thing, see this." }' # Remove an item at position curl -X DELETE "https://neighborhood-commons.org/api/v1/service/lists/LIST_UUID/items/3" \ -H "X-API-Key: $NC_KEY" ``` ### 4.9 Copyright and image rights The Commons re-hosts images on its own infrastructure (R2) and serves them to every consumer that reads the affected event. We assert CC BY 4.0 on the data, including images, **based on contributor warranties** — we do not pre-screen uploads against copyright databases today. **Contributor warranty.** When you POST an `image_url`, you warrant that (a) you own the rights or have permission to redistribute under CC BY 4.0, (b) the content isn't infringing, and (c) you indemnify the Commons against claims arising from your upload. **Safe content:** posters and flyers your venue or community group designed; photos you took or licensed; permissively-licensed content with attribution preserved. **Unsafe content:** Google Maps photos (Google owns those); stock images without license; press photos from artist/label/studio sites without authorization; newspaper/blog images; AI-generated images using copyrighted training material in a way the platform's terms forbid. When in doubt, omit the image. Text-only events are better than takedown notices. **Designated DMCA Agent (17 U.S.C. § 512(c)(2)):** ``` Service Provider: Zachary Benjamin Designated Agent: Zachary Benjamin Address: 937 N 2nd St, 3F, Philadelphia, PA 19123, USA Phone: 503-449-5572 Email: dmca@neighborhood-commons.org Registration No: DMCA-1072738 Effective: May 14, 2026 to present ``` **Takedown procedure.** Email `dmca@neighborhood-commons.org` with: (1) identification of your work, (2) identification of the infringing material on the Commons (event ID, image URL), (3) your contact info, (4) a good-faith statement that the use is unauthorized, (5) a statement under penalty of perjury that your information is accurate, (6) signature. **Our commitment:** acknowledgment within 24 hours, substantive review within 48 hours, image removal + image_url nulled if substantiated, notification to the contributor, optional counter-notification flow per 17 USC §512. **Repeat-infringer policy:** - 1st substantiated: warning, image removed, internal record. - 2nd: service key suspended; reinstatement requires operator conversation. - 3rd: service key revoked; re-activation requires new application and demonstrated remediation. **For consumers reading the Commons:** CC BY 4.0 is asserted in good faith based on warranties, not pre-verification. Display images at your discretion. Higher-confidence images come from verified businesses publishing their own content. Filter on `?first_party=true` if rights-cleanness matters more than coverage. --- ## 5. Verification Verification has one job: **anchor first-party authority for an organization's profile data (Type A).** Confirm that an organization claiming to be a real-world entity actually corresponds to it. That's the entirety of the load. What verification is not: - Not a cross-app reputation graph. "Verified by N apps" is not a meaningful signal the Commons exposes or computes. - Not an identifier portability mechanism. The Commons does not maintain portable verification credentials across apps. - Not a network-effect prize. The anti-extraction work is done by CC-BY plus many readers (the multiplicative thesis), not by verification machinery. The Commons orchestrates the verification process; apps surface the UI; the verified organization becomes the only valid editor of its Type A profile data. ### 5.1 The flow **Step 1 — Discover the path.** Given an organization and identifier, the Commons returns which submission endpoint to call: ```bash curl "https://neighborhood-commons.org/api/v1/service/verifications/path?\ organization_id=ORG_UUID&\ identifier_type=email&identifier_value=manager@bubs.example" \ -H "X-API-Key: $NC_KEY" ``` Response shapes: ```json // Business-domain email { "alreadyVerified": false, "requiredMethod": "domain_email_loop", "endpoint": "/v1/service/verifications/challenges" } // Personal-domain email (gmail, yahoo, etc.) — needs evidence-based review { "alreadyVerified": false, "requiredMethod": "manual_review", "endpoint": "/v1/service/verifications/manual" } // Already verified { "alreadyVerified": true, "existingVerification": { "method": "domain_email_loop", "verifiedAt": "...", "verifiedByApp": "Holler" } } ``` ### 5.2 Auto-track (challenge / confirm) Used when path returned `domain_email_loop`. Issue a code; the Commons emails it from your `brand_config` sender: ```bash curl -X POST "https://neighborhood-commons.org/api/v1/service/verifications/challenges" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "organizationId": "ORG_UUID", "identifierType": "email", "identifierValue": "manager@bubs.example" }' # Response: { "challengeId": "...", "expiresAt": "..." } # User pastes the code in your UI; you submit curl -X POST "https://neighborhood-commons.org/api/v1/service/verifications/challenges/CHALLENGE_ID/confirm" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "code": "123456" }' ``` Verification-challenge codes are six digits (distinct from the OTP used by the developer portal at `/developers/sign-up` for key issuance — different flows, different code lengths). Challenges expire after 30 minutes. Five wrong attempts on the same challenge return `429 RATE_LIMIT`. ### 5.3 Manual track (structured evidence) Used when path returned `manual_review` — for organizations without clean email-loop access (community groups, neighborhood associations, civic bodies). Submit evidence; if your key has `verification_authority` covering the method, it auto-approves; otherwise it queues for admin review: ```bash curl -X POST "https://neighborhood-commons.org/api/v1/service/verifications/manual" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "organizationId": "ORG_UUID", "identifierType": "email", "identifierValue": "secretary@phillychessclub.org", "evidence": { "verifiedVia": "documentation_review", "reviewerAttestation": "Reviewed bylaws and meeting minutes; identifier matches secretary listed in 2024 filing.", "reviewerAccountId": "internal-reviewer-id" } }' ``` Acceptable evidence types depend on the organization. For community groups: bylaws, meeting minutes, listed contact in long-standing public materials. For businesses: business license, address verification, in-person visit. The reviewer attestation captures the reasoning for audit. ### 5.4 verification_authority A JSON array on each service key listing the verification methods the key may auto-approve. Set during activation, based on the verification process you described. Format: `method:context`. | Value | Meaning | |---|---| | `["manual_review:in_person"]` | Auto-approve manual reviews where `verifiedVia` is `in_person`. Queues for admin otherwise. | | `["manual_review:in_person", "manual_review:video_call"]` | Both methods auto-approve. | | `null` / `[]` | No authority. All manual submissions queue for admin review. | | `["*"]` | Operator/admin wildcard. Reserved for Studio. | Apps without authority can still submit manual-review evidence — those submissions just queue. Typical turnaround for admin review is one business day. --- ## 6. Webhooks Subscribe to push notifications instead of polling. Each delivery is HMAC-SHA256 signed. Failed deliveries are retried with exponential backoff; subscriptions that fail repeatedly are auto-disabled. ### 6.1 Setup Subscribe with a service-tier API key: ```bash curl -X POST "https://neighborhood-commons.org/api/v1/webhooks" \ -H "X-API-Key: $NC_KEY" -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.example/webhooks/commons", "event_types": ["event.created", "event.updated", "event.deleted"] }' # Response includes a signing_secret. Save it — it cannot be recovered. # Test the endpoint with a synthetic delivery curl -X POST "https://neighborhood-commons.org/api/v1/webhooks/SUBSCRIPTION_ID/test" \ -H "X-API-Key: $NC_KEY" # Inspect recent delivery attempts curl "https://neighborhood-commons.org/api/v1/webhooks/SUBSCRIPTION_ID/deliveries" \ -H "X-API-Key: $NC_KEY" # Unsubscribe curl -X DELETE "https://neighborhood-commons.org/api/v1/webhooks/SUBSCRIPTION_ID" \ -H "X-API-Key: $NC_KEY" ``` ### 6.2 Event types | Type | Fires on | |---|---| | `event.created` | New event row published. | | `event.updated` | Event mutation (PATCH). | | `event.deleted` | Event hard-deleted. | | `event.series_created` | Recurring series instantiated. Payload carries the new series identity (`series.id/slug/name/description/cover_image_url`) so subscribers can hydrate without a follow-up fetch. | | `series.updated` | Series identity changed (name, slug, description, or cover image). Payload includes the new `series` profile plus a `changed` array listing the fields that changed. Use this to invalidate cached series pages. | | `series.deleted` | Series removed. Complements the per-instance `event.deleted` events that also fire. Payload carries `series.{id,slug,name}` at time of deletion plus `instance_ids[]`. | ### 6.3 Payload shape ```json { "type": "event.created", "delivery_id": "...", "subscription_id": "...", "occurred_at": "2026-05-05T15:00:00Z", "data": { /* the resource in spec shape — Event, Organization, etc. */ } } ``` The resource's `organizer` block is always an organization reference, including the `verified` boolean. Consumer apps with tier-rendering logic can compute their tier solely from `event.first_party` plus `event.organizer.verified` — no additional verification lookup needed. ### 6.4 Signature verification Each delivery carries an `X-NC-Signature` header containing the HMAC-SHA256 of the raw request body, keyed by your subscription's `signing_secret`. ```typescript import { createHmac, timingSafeEqual } from 'crypto'; function verifyWebhook(rawBody: Buffer, signatureHeader: string, secret: string): boolean { const [algo, sig] = signatureHeader.split('='); if (algo !== 'sha256' || !sig) return false; const expected = createHmac('sha256', secret).update(rawBody).digest('hex'); const sigBuf = Buffer.from(sig, 'hex'); const expectedBuf = Buffer.from(expected, 'hex'); if (sigBuf.length !== expectedBuf.length) return false; return timingSafeEqual(sigBuf, expectedBuf); } ``` Verify against the *raw* request body, not a re-stringified version — different JSON serializers produce different byte sequences. ### 6.5 Retry behavior Deliveries that fail retry with exponential backoff: ~30s, 2m, 10m, 1h, 6h. After five consecutive failures, the subscription is automatically disabled; re-enable via `PATCH` once your endpoint is healthy. ### 6.6 Idempotency The Commons may retry a delivery that actually succeeded (e.g., your server returned 2xx but Commons didn't see the response). Use `delivery_id` as your idempotency key. --- ## 7. SDK & spec The official SDK is a typed TypeScript client generated from `/openapi.json`. Install: ```bash npm install neighborhood-commons ``` The SDK is intentionally thin: generated types and a typed `fetch` wrapper. ~50 lines of runtime, zero opinions beyond the spec. ### Version pinning and stability `neighborhood-commons@3.x` is the current major version. Within a major, the spec is additive-only — what works today works in 18 months. Breaking changes require a major bump and are measured in years, not months. The npm package version aligns with the spec major version. ### Generating clients in other languages ```bash # TypeScript types only (matches what we publish) npx openapi-typescript https://neighborhood-commons.org/openapi.json -o ./types.ts # Full clients in any supported language openapi-generator-cli generate -i https://neighborhood-commons.org/openapi.json -g python -o ./client openapi-generator-cli generate -i https://neighborhood-commons.org/openapi.json -g go -o ./client openapi-generator-cli generate -i https://neighborhood-commons.org/openapi.json -g rust -o ./client ``` --- ## 8. Sustainability The Commons exists to enable sustainable businesses (apps, publications, tools) built on top of shared neighborhood public-fact infrastructure. Direction is responsive to early participants; funding pathways are open. One designed sustainability mechanism is classifieds — structured public offers (jobs, housing, services, lost-and-found) from organizations, distributed for a fee through participating local publications: - **Anti-monopolistic by construction.** Publications set their own per-ad rates and choose their own accepted categories. Competition between publications keeps rates honest. - **Anti-surveillance by design.** Targeting is by app/publication affinity (a self-declared audience signal), never by individual user behavior. - **Aligned with local media's interest.** Revenue flows from advertiser through the Commons to the participating publication, with the Commons taking a small infrastructure cut. Classifieds are designed, not yet built. See [`docs/classifieds.md`](https://github.com/joinfiber/neighborhood-commons/blob/master/docs/classifieds.md) for the full design. Other funding pathways are valid too: grant funding (civic infrastructure is grant-eligible), foundation partnerships (a journalism-affiliated foundation could shape the Commons toward press-aligned features), participant cost-sharing (multiple apps/publications drawing on the substrate could cooperatively fund operations). The Commons doesn't need to win a single funding pathway to succeed — it needs to be useful enough to participants that some combination of these emerges as it grows. --- ## 9. Reference The complete endpoint list, request/response schemas, and error enum live in `/openapi.json`. That document is authoritative — when this Guide and the spec disagree, trust the spec. ### 9.1 Endpoint summary — read No authentication required. | Method | Path | |---|---| | `GET` | `/v1/events` · `/v1/events/:id` · `/v1/events.ics` · `/v1/events.rss` | | `GET` | `/api/events/changes` | | `GET` | `/v1/places` · `/v1/places/:id` | | `GET` | `/v1/organizations` · `/v1/organizations/:idOrSlug` | | `GET` | `/v1/publishers` · `/v1/publishers/:idOrSlug` | | `GET` | `/v1/broadcasts` · `/v1/broadcasts/:id` | | `GET` | `/v1/lists` · `/v1/lists/:idOrSlug` | | `GET` | `/v1/meta` · `/v1/meta/regions` · `/v1/meta/categories` · `/v1/meta/tags` · `/v1/meta/stats` | ### 9.2 Endpoint summary — service tier Require `X-API-Key` header with a service-tier key. | Method | Path | |---|---| | `POST` | `/v1/service/places` | | `POST` · `PATCH` | `/v1/service/organizations` · `/v1/service/organizations/:id` | | `POST` | `/v1/service/organizations/link` | | `POST` | `/v1/service/organizations/:id/logo` · `/v1/service/organizations/:id/image` | | `POST` | `/v1/service/broadcasts` · `/v1/service/broadcasts/:id/retract` | | `POST` · `PATCH` | `/v1/service/lists` · `/v1/service/lists/:id` | | `POST` · `DELETE` | `/v1/service/lists/:id/items` · `/v1/service/lists/:id/items/:position` | | `POST` · `PATCH` · `DELETE` | `/v1/service/events` · `/v1/service/events/:id` | | `PATCH` | `/v1/service/events/:id/organizer` | | `POST` | `/v1/service/events/:id/image` | | `GET` | `/v1/service/verifications/path` | | `POST` | `/v1/service/verifications/challenges` · `/v1/service/verifications/challenges/:id/confirm` | | `POST` | `/v1/service/verifications/manual` | | `GET` · `POST` | `/v1/service/verifications/pending` · `.../:id/approve` · `.../:id/reject` (admin) | | `POST` · `GET` · `DELETE` | `/v1/webhooks` · `/v1/webhooks/:id` · `/v1/webhooks/:id/test` · `/v1/webhooks/:id/deliveries` | ### 9.3 Error codes Every error response has the same envelope: ```json { "error": { "code": "VALIDATION_ERROR", "message": "Human-readable explanation." } } ``` Codes you'll commonly see: | Status | Code | Meaning | |---|---|---| | 400 | `VALIDATION_ERROR` | Request body or query failed schema validation. | | 400 | `INVALID_URL` · `INVALID_SCHEME` · `BLOCKED_HOSTNAME` · `IP_LITERAL` · `URL_CREDENTIALS` | URL field failed safety checks. | | 401 | `UNAUTHORIZED` · `API_KEY_REQUIRED` · `INVALID_API_KEY` | Auth required or invalid. | | 403 | `KEY_PENDING` | Service key is pending activation. | | 403 | `NOT_LINKED` | Service key is not linked to the target organization. | | 403 | `INSUFFICIENT_TIER` | Endpoint requires admin or higher tier than the calling key has. | | 404 | `NOT_FOUND` | Resource doesn't exist or isn't visible to the caller. | | 409 | `WRONG_METHOD` | Verification submission posted to the wrong path. | | 409 | `CONFLICT` · `DUPLICATE` | State conflict, including slug collisions and duplicate `external_id`. | | 413 | `PAYLOAD_TOO_LARGE` | Body exceeds the per-route limit. | | 429 | `RATE_LIMIT` | Window exceeded. Back off; check `RateLimit-*` headers. | | 500 | `SERVER_ERROR` | Internal failure. No internal details exposed. | The complete `ErrorCode` enum is in `/openapi.json`. Generated SDK clients receive it as a discriminated union. ### 9.4 Stability The 3.0.0 spec is stable. Future minor versions add types, fields, and endpoints additively without breaking existing consumers. Removals or renames require a 4.0.0 release with strong justification. If something in this Guide contradicts `/openapi.json`, trust the spec. --- ## Appendix A: Categories 20 event categories. Use the slug (left column) in API requests. Both kebab-case (`live-music`) and underscore (`live_music`) are accepted on input. Responses always use kebab-case. | Slug | Label | Group | |------|-------|-------| | `live-music` | Live Music | Performance | | `dj-dance` | DJ & Dance | Performance | | `comedy` | Comedy | Performance | | `theatre` | Theatre | Performance | | `open-mic` | Open Mic | Performance | | `karaoke` | Karaoke | Performance | | `art-exhibit` | Art & Exhibits | Arts & Culture | | `film` | Film | Arts & Culture | | `literary` | Literary | Arts & Culture | | `tour` | Tour | Arts & Culture | | `happy-hour` | Happy Hour | Food & Drink | | `market` | Market & Pop-up | Food & Drink | | `fitness` | Fitness | Active | | `sports` | Sports & Rec | Active | | `outdoors` | Outdoors & Nature | Active | | `class` | Class & Workshop | Learning & Social | | `trivia-games` | Trivia & Games | Learning & Social | | `kids-family` | Kids & Family | Learning & Social | | `community` | Community | Civic | | `spectator` | Spectator | Civic | --- ## Appendix B: Tags ### Event tags 30 prescribed tags plus any custom tags. Tags describe the experience of attending — "Can I go? What's the space like? What's the energy?" **Access — "Can I go?"** `all-ages`, `18-plus`, `21-plus`, `family-friendly`, `free`, `cover-charge`, `donation-based`, `na-friendly`, `byob`, `dog-friendly`, `cash-only` **Logistics — "How do I attend?"** `registration-required`, `drop-in`, `limited-spots`, `solo-friendly`, `bring-gear` **Setting — "What's the space like?"** `outdoor`, `rooftop`, `seated` **Vibe — "What's the energy?"** `chill`, `high-energy`, `late-night`, `beginner-friendly`, `themed`, `competitive` **Format — "What happens there?"** `hands-on`, `tasting`, `acoustic`, `participatory`, `volunteer` **Rules:** - Age tags (`all-ages`, `18-plus`, `21-plus`) are mutually exclusive. - Max 15 tags per event. - Custom tags accepted alongside prescribed ones. Must be lowercase kebab-case, max 100 characters. - Each category has an approved subset of prescribed tags; tags not approved for the category are silently removed. ### Organization tags Recommended starter vocabulary for `organizations.tags`. Free-form within format rules (lowercase kebab-case). Published at `/v1/meta/tags` so consumer apps can introspect. `music-venue`, `restaurant`, `cafe`, `bar`, `retail`, `salon`, `fitness`, `community-group`, `religious`, `civic`, `arts-culture`, `outdoor-space`, `nonprofit`, `curator`, `solo-act` This list grows by deliberate addition as patterns emerge from actual publisher usage. Apps may filter on any tag including unrecognized ones; the recommended list is guidance, not a hard constraint. --- ## Appendix C: Regions Every event belongs to a geographic region (a city or neighborhood). When you submit an event: 1. If you provide coordinates (`location.lat` + `location.lng`) — the Commons resolves the containing region automatically. 2. If you provide only an address — the Commons geocodes it first, then resolves the region. 3. If neither is provided — the event gets the default region. Events without a proper region won't appear in location-filtered feeds. Always provide an address or coordinates. `GET /v1/meta/regions` lists active regions. --- ## License & contact All data is published under **Creative Commons Attribution 4.0 International (CC BY 4.0)**. You can use it for any purpose — commercial or non-commercial — as long as you credit "Neighborhood Commons." Place categorization sourced from **OpenStreetMap** under the **Open Database License (ODbL)**. Attribution: © OpenStreetMap contributors. The source is open under MIT: [github.com/joinfiber/neighborhood-commons](https://github.com/joinfiber/neighborhood-commons). Questions: hi@neighborhood-commons.org Spec we follow: [Neighborhood API v0.2](https://github.com/The-Relational-Technology-Project/neighborhood-api).