API

Public JSON API spanning every section of black.wiki — apps, news, directory, playbooks, homage, sayings, polls, jobs, the yard, and more. Read is open; writes need a token.

Quick start

All endpoints live under https://black.wiki/api. Responses are JSON, CORS is open, and read endpoints are cached for ~60s.

curl https://black.wiki/api/apps
curl https://black.wiki/api/news
curl https://black.wiki/api/sayings/random

Authentication

Read endpoints are open. Writes (anything marked AUTH on its row) need a token sent as a bearer header. Two flavors are accepted:

1 · Generate a token

  1. Sign in to black.wiki, then go to /account/tokens.
  2. Click New token, give it a label so you can recognize it later (e.g. "chrome-extension" or "ci-bot").
  3. Copy the token immediately. It starts with bwk_ and is shown exactly once — closing the dialog without copying means you have to regenerate.

2 · Send it with each request

Add an Authorization header with the value Bearer <your-token>.

# cURL
curl https://black.wiki/api/me \
  -H "Authorization: Bearer bwk_xxxxxxxxxxxxxxxxxxxxxxxx"

# JavaScript (fetch)
const res = await fetch("https://black.wiki/api/me", {
  headers: { Authorization: `Bearer ${token}` },
});

# Python (requests)
import requests
r = requests.get(
  "https://black.wiki/api/me",
  headers={"Authorization": f"Bearer {token}"},
)

3 · Submit a write (example)

POST endpoints take a JSON body. Always set Content-Type: application/json alongside the bearer header.

curl -X POST https://black.wiki/api/sayings/submit \
  -H "Authorization: Bearer bwk_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "If it cost you your peace, it was too expensive.",
    "meaning": "Stop chasing things that drain you.",
    "region": "South"
  }'

Error responses

Best practices

Conventions

Endpoints by section

Click a section to expand. Looking for the old single-page docs? /docs/api/v1.

Apps

4 endpoints

Community-built tools and extensions listed in /directory/apps.

GET/api/apps

List published apps that have the JSON API enabled.

Query

  • qFilter by name (case-insensitive).
  • limitDefault 50, max 100.
  • offsetPagination offset.
GET/api/apps/{slug}

Single app with its public metadata.

GET/api/apps/{slug}/files

List the JSON data files the app owner has uploaded.

GET/api/apps/{slug}/data/{filename}

Fetch a single data file by name.

News

2 endpoints

Published news posts including both editorial and aggregated articles.

GET/api/news

List published news posts, newest first.

Query

  • qFull-text search title + excerpt.
  • sourceFilter by source_name.
  • limitDefault 30, max 100.
  • offsetPagination offset.
GET/api/news/{slug}

Single news post with body HTML.

Directory

5 endpoints

People, businesses, landmarks, services, and events. Each kind has its own URL path. Businesses and landmarks have curated subcategory taxonomies — fetch them once from /api/directory/categories and use them with ?category= here. If you're building an agent that doesn't know in advance whether a thing is a business or a landmark (commerce vs cultural-historic), call /api/directory/search instead and it'll match across kinds. The legacy /directory/places endpoints still work — they 301 to either /landmarks or /businesses depending on the entry.

GET/api/directory/search

Cross-kind search. Same tokenized + synonym-expanded text search as the per-kind endpoints, but matches across business, landmark, service, and event by default. Each row includes its kind + kind_slug so callers can route. Best choice for AI agents: one call, no need to guess the category up front.

Query

  • qFree-text search. Tokenized + synonym-expanded — "food" matches Restaurant places, "drinks" matches Bar / Lounge, etc.
  • kindsComma-separated subset of business,landmark,service,event,person. Default: business,landmark,service,event. The legacy value 'place' is accepted for backwards compatibility and quietly expanded to business + landmark.
  • zip5-digit US ZIP to filter by metro. Same heuristic as /api/directory/{category}.
  • limitDefault 20, max 100.
  • offsetPagination offset.
GET/api/directory/categories

Curated subcategory taxonomies, keyed by kind: { businesses, landmarks, services, events, people }. Use these exact strings with ?category= on /api/directory/{category}. Cached one hour.

GET/api/directory/{category}

List published entries in a category. Category is the plural slug: businesses, people, places, services, events. Response includes available_categories so clients can render filter chips without a separate lookup, and filtered_by_zip when ?zip= is set.

Query

  • qFree-text search. Tokenized on whitespace; each word must match name, summary, category, city, or region (ilike). So ?q=Tampa+restaurant returns places in Tampa with category Restaurant — you do NOT need the entry name to contain both words.
  • letterFilter by first letter (a-z or #).
  • categoryFilter by curated subcategory. Repeat or comma-separate for OR (e.g. ?category=Restaurant,Café). Unknown values are dropped.
  • zip5-digit US ZIP. Resolves to the closest big-city metro (within 120 km) and filters by that city OR same-state. Falls back gracefully if the ZIP can't be resolved.
  • limitDefault 24, max 100.
  • offsetPagination offset.
GET/api/directory/people

Shortcut for the people category.

GET/api/directory/jobs

Open + filled job listings.

Query

  • kindFilter by job kind.
  • remotetrue to filter remote-only.

Playbooks (Level Up)

2 endpoints

Community guides and game plans.

GET/api/playbooks

List published playbooks.

Query

  • topicFilter by topic slug.
  • qFilter by title.
  • limitDefault 30, max 50.
  • offsetPagination offset.
GET/api/playbooks/{slug}

Single playbook with body HTML and quiz JSON.

Homage

3 endpoints

Tributes to people, films, music, brands, and other contributions worth honoring.

GET/api/homage

List published homage entries.

Query

  • qFilter by name.
  • catFilter by category slug.
  • letterFilter by first letter.
  • limitDefault 24.
  • offsetPagination offset.
GET/api/homage/{slug}

Single homage entry.

GET/api/homage/categories

Category dictionary used by /api/homage cat filter.

Sayings

5 endpoints

Phrases, proverbs, and one-liners from Black culture.

GET/api/sayings

List published sayings.

Query

  • qFilter by text + meaning.
  • categoryFilter by category slug.
  • regionFilter by region tag.
  • limitDefault 24.
  • offsetPagination offset.
GET/api/sayings/{slug}

Single saying with meaning + attribution.

GET/api/sayings/random

One random saying. Good for widgets / extensions.

GET/api/sayings/categories

Category dictionary.

POST/api/sayings/submitAUTH

Submit a saying for moderator review. Goes into the queue at /admin/sayings/queue.

Body

  • textstringThe saying itself.
  • meaningstring?Plain-English explanation.
  • attributionstring?Optional credit (e.g. "Old-school Black uncle").
  • regionstring?South / Midwest / etc.
  • categorystring?Category slug.

Polls

3 endpoints

Daily community polls with question + options + tallies.

GET/api/polls

List open polls, newest first.

Query

  • statusopen | closed (default: open).
  • limitDefault 30.
  • offsetPagination offset.
GET/api/polls/{slug}

Single poll with options + current vote tallies.

POST/api/pollsAUTH

Submit a community poll. Requires a signed-in user (Supabase session cookie or bearer). When the admin toggle polls_require_approval is on, the poll lands as 'draft' awaiting review; otherwise it publishes instantly. Response includes pending_review when held for moderation.

Body

  • questionstringPoll question. Max 280 chars.
  • descriptionstringOptional context shown above the options.
  • optionsstring[]2-8 option labels. Empty values are dropped.
  • allow_commentsbooleanDefault true. Set false to disable per-vote comments.

The Yard (points shop)

4 endpoints

Catalog of items redeemable with points. Read-only; redemption is web-only.

GET/api/the-yard

List active shop items.

Query

  • qFilter by title.
  • limitDefault 24.
  • offsetPagination offset.
GET/api/the-yard/{slug}

Single shop item with description + point cost.

GET/api/yard/submitAUTH

Current submission fee (sliding cost based on community size) and the caller's eligibility.

POST/api/yard/submitAUTH

Submit a community-donated Yard item. Goes to the admin review queue.

Body

  • titlestringItem title.
  • descriptionstring?Optional rich description.
  • image_urlstring?Optional cover image.
  • point_costintCost in points to redeem.
  • stockint?Null = unlimited.
  • max_per_userint?Null = no per-user cap.
  • fair_market_value_usdint?Approximate FMV (1099 reporting).

Community moderators

2 endpoints

Read the current slot config + roster; sign-in required to claim a slot.

GET/api/community-mods

Config (slots, term days, cost), currently held terms, and the viewer's eligibility.

POST/api/community-mods/claimAUTH

Claim an open slot. Debits the configured point cost.

Leaderboard

1 endpoint

Public ranking of contributors by points earned.

GET/api/leaderboard

Top contributors, points-descending. Excludes opt-out users.

Query

  • limitDefault 50, max 100.
  • offsetPagination offset.

Me (signed-in)

3 endpoints

The caller's own data. All endpoints require a user token.

GET/api/meAUTH

Current profile + role flags + points balance.

GET/api/me/pointsAUTH

Points event log for the caller.

Query

  • limitDefault 50.
  • offsetPagination offset.
GET/api/me/tokenAUTH

Cookie-only. Returns the active Supabase session (access_token + refresh_token + expires_at) so a signed-in browser can hand the session to a mobile/native client. The client feeds the pair to supabase.auth.setSession(...) and then talks to the rest of /api with the access token as Authorization: Bearer.

Content reports

1 endpoint

Flag a piece of content for moderator review. Rate-limited and dedupe per (target, reporter).

POST/api/reportAUTH

Submit a report. Three open reports against a target auto-hide it pending review.

Body

  • target_typeenumdirectory_entry | app | job | playbook | homage_entry | news_post
  • target_iduuidTarget row id.
  • reason_categoryenumnot_black_owned | misleading | spam | duplicate | abusive | other
  • reason_detailstring?Free-text up to 1000 chars.

Geocoding

2 endpoints

Server-side proxies so clients don't have to hit third-party APIs directly. Both responses are cached aggressively (ZIPs are static; reverse-geocodes are cached one minute).

GET/api/geocode/zip

Resolve a 5-digit US ZIP to lat/lng + city + state, then snap to the closest big-city metro (within 120 km). Same metro the Near me button and the search page's ZIP filter use. Returns { zip, city, region, metro_city, metro_region, lat, lng }.

Query

  • zipRequired. 5-digit US ZIP.
GET/api/geocode/reverse

Reverse-geocode a (lat, lng) pair. By default snaps to the closest curated big city within 120 km (good for "near Tampa"-style header text). Pass precise=1 to skip the snap and return the locality the GPS coords actually land in (Apollo Beach, Brandon, etc.). Always returns the snapped metro alongside under metro.{city, region}. Response: { city, region, country, metro }.

Query

  • latRequired. -90 to 90.
  • lngRequired. -180 to 180.
  • preciseOptional. 1 = skip the 120 km metro snap and return Nominatim's literal locality (e.g. Apollo Beach). Default 0.

On Stage

2 endpoints

Curated spotlight features pointing to people, businesses, or works in the directory. Same ordering and payload the public /on-stage page uses.

GET/api/on-stage

List published features in display order (sort_order asc, published_at desc).

Query

  • limitDefault 60, max 100.
GET/api/on-stage/{slug}

Single feature by entry slug. Includes the caption, Threads permalinks, cover image, and the underlying directory entry.

Games

9 endpoints

Mobile-friendly game endpoints. The index lists what's available; per-game GETs return the current daily state and POSTs record the user's play. Plays require a user token; reading the daily question/pairs is open.

GET/api/games

Static index of available games with the endpoints that drive each one.

GET/api/games/trivia

Today's trivia question + (for auth'd callers) whether they've already played and the result.

POST/api/games/triviaAUTH

Record the caller's play for today. Returns 409 if they've already played.

Body

  • question_iduuidFrom the GET response.
  • correctbooleanWhether the answer was right.
GET/api/games/match

Today's 4 Match pairs + (for auth'd callers) whether they already played, won/lost, and attempts.

POST/api/games/matchAUTH

Record today's Match play. Returns 409 if already played.

Body

  • wonbooleanDid they solve all pairs?
  • attemptsint?Number of guess attempts.
GET/api/games/wordle

Today's 5-letter answer (rotates daily by UTC date) + the auth'd caller's play state. 6 max guesses.

POST/api/games/wordleAUTH

Record today's Wordle play. Caller validates guesses locally; server only stores the final outcome. 409 if already played.

Body

  • wonbooleanDid they solve the word?
  • attemptsint?Number of guesses used (1-6).
GET/api/games/guess

Today's mystery item (drawn from published apps + directory entries), a staged clue list, and the auth'd caller's play state. 4 max guesses.

POST/api/games/guessAUTH

Record today's Guess the Listing play. 409 if already played.

Body

  • wonbooleanDid they guess the item?
  • attemptsint?Number of guesses used (1-4).

Nearby (GPS discovery)

1 endpoint

Unified "what's around me" feed across the directory + On Stage. Every published entry is geocoded; results are sorted by true haversine distance from the caller's coords. Each item carries distance_km. If the caller is in a region with no geocoded entries (mid-ocean, far rural), the response falls back to the legacy metro-snap filter so it never returns empty. resolved_metro is always the nearest curated big city for header text.

GET/api/nearby

Returns { resolved_city, resolved_region, resolved_metro, radius_km, cities, items }. items is a mixed list with a 'kind' discriminator: business, landmark, place, person, event, on_stage. Each item includes distance_km (rounded to 0.1 km) along with address, website, event dates, and lat/lng where the row has them.

Query

  • latRequired. -90 to 90.
  • lngRequired. -180 to 180.
  • radius_kmDefault 80, max 500.
  • kindsComma-separated subset of business,landmark,place,person,event,on_stage. Default: all.
  • limitDefault 30, max 100.

Timeline

1 endpoint

Year-ordered mosaic of every published directory entry that has a year_start. Same data /timeline renders on the web, plus the era bands so a mobile client can draw the same tinted backdrop without duplicating the era table.

GET/api/timeline

Returns { items, count, eras, filters }. Each item has the entry's slug, name, kind, year_start/end, summary, and a page_url. Eras are the curated bands (Early Diaspora, Atlantic Slavery, Reconstruction, Jim Crow, Great Migration, Harlem Renaissance, Civil Rights, etc.).

Query

  • fromEarliest year_start (inclusive).
  • toLatest year_start (inclusive).
  • kindsComma-separated subset of business,person,landmark,place,event,service. Default: all.
  • eraEra slug from the eras response — narrows from/to to that era's bounds.
  • limitDefault 1000, max 2000.

Product reviews

1 endpoint

Flat feed of approved product reviews across the site. Reviews are also embedded in /api/products/{slug}; this endpoint is for a chronological "Reviews" tab.

GET/api/reviews

Approved reviews newest-first. Each item is denormalized with the product slug, name, and image so the client can render without a second fetch.

Query

  • limitDefault 30, max 100.
  • offsetPagination offset.
  • min_rating1–5. Filter to reviews at or above this rating.
  • productProduct slug. Filter to one product.

Ask (natural-language search)

1 endpoint

One endpoint behind the floating chat widget. Pass a user question + optional coordinates and stream back a directory-grounded answer plus the matches it cited. Results come strictly from black.wiki — the model is constrained to never invent businesses. Same endpoint Persona One will call for voice/SMS.

POST/api/ask

Returns an NDJSON stream — one JSON object per line — so clients can render tokens as they arrive. Event types: 'meta' (matches + parsed intent + resolved viewer metro), 'delta' (a text fragment of the answer), 'done' (final answer + cited slugs), 'error' (rate limit / extraction failure). Non-streaming consumers can buffer the full body and read just the meta + done lines. Rate-limited to 12 req/min per IP.

Body

  • querystringUser's question. Max 500 chars.
  • latnumber?Latitude. Used to bias matches to the viewer's metro.
  • lngnumber?Longitude.

Fake content reports

2 endpoints

Community-watch intake for fake profiles, AI-generated content, impersonation, scams, and misinformation found OFF black.wiki (on social platforms etc.). Anyone can submit — signed-in or anonymous. The public feed returns only reports a moderator has confirmed, with all submitter info stripped.

GET/api/report-fake

Public awareness feed — confirmed reports only, submitter PII removed. Cached.

Query

  • categoryFilter to one of: fake_profile, ai_generated, impersonation, scam, misinformation, other.
  • limitDefault 50, max 100.
POST/api/report-fake

File a report. No account required (signed-in callers are auto-linked). Runs the same moderation + IP-block defenses as feedback.

Body

  • categorystringOne of: fake_profile, ai_generated, impersonation, scam, misinformation, other.
  • descriptionstringWhat's wrong with it. 3–4000 chars.
  • platformstring?Where it was seen (e.g. "Threads", "TikTok").
  • content_urlstring?Link to the offending profile or content.
  • handlestring?The account name or handle.
  • submitter_namestring?Optional, for credit/follow-up.
  • submitter_emailstring?Optional, never shown publicly.

Business shout-outs

2 endpoints

Public "Did you support a Black business today?" shout-outs left from the home page. Anyone can post — signed-in or anonymous. The feed returns approved shout-outs with submitter PII stripped (display name only). To link a shout-out to a real directory business, autocomplete with GET /api/directory/businesses?q= and pass the chosen entry's id as directory_entry_id. If you omit it and name a business that isn't listed yet, a signed-in post creates a draft directory listing (pending admin review).

GET/api/business-shoutouts

Approved shout-outs, newest first. business_slug is set when the linked business is published (build /directory/businesses/{business_slug}). Cached.

Query

  • limitDefault 12, max 100.
  • offsetPagination offset.
POST/api/business-shoutouts

Leave a shout-out. No account required (signed-in callers are auto-linked, and a later signup from the same browser adopts anonymous shout-outs). Response includes signedIn so the client can nudge account creation.

Body

  • business_namestringThe business. 1–120 chars.
  • ratingnumber1–5 stars.
  • citystring?Where the business is.
  • bodystring?A quick word. Max 1000 chars.
  • submitter_namestring?Optional display name.
  • directory_entry_idstring?UUID of a directory business (from /api/directory/businesses autocomplete) to link this shout-out to.

Media kit

1 endpoint

Everything a “support / spread the word” screen needs: the site QR code, downloadable brand assets, printable sheets (poster, stickers, storefront sign), and merch (print-on-demand) links. All URLs absolute.

GET/api/media-kit

Site QR (png + svg), brand_assets, brand_colors, printables, and merch links. merch[].available is false until a store URL is configured.

Admin

1 endpoint

Staff-only endpoints. Require a moderator/admin session (cookie or Bearer token); non-staff get 403.

GET/api/admin/recent-submissionsADMIN

Latest user submissions across every area (flags, feedback, comments, reviews, directory entries, games, reports, shout-outs, and more), merged newest-first. Returns { items, sources, skipped }.

Query

  • limitDefault 80, max 200.
  • sourceNarrow to one area key (see the sources array in the response).
API · black.wiki