Build on The Shooters Hub

Open APIs, flexible embeds, and tools that keep shooters in sync.

Everything you need to power custom dashboards, automate match promotion, or embed the finder on your own site. Start with the public endpoints, request keys when you are ready, and ship faster with our community behind you.

API rate plans

Every key starts on the Community tier. When your integration needs more headroom, pick a plan and we’ll help you upgrade without breaking existing clients.

Community

$0

Default tier for self-serve developers building dashboards, widgets, or local tools.

Requests / minute120
Requests / day8,000
Requests / month200,000

Best for: Personal keys (default)

Partner

Let’s chat

Higher ceilings for approved partners syncing archives or powering public discovery tools.

Requests / minute300
Requests / day25,000
Requests / month600,000

Best for: Trusted integrations

Enterprise

Custom

Commercial data products, national federations, and partners who need burst headroom.

Requests / minute900
Requests / day90,000
Requests / month1,800,000

Best for: Commercial workloads

Club, match, season, and series scoped keys already include conservative per-entity caps (20–60 requests per minute) to keep integrations honest. Reach out if you need a temporary boost.

Embed the match finder anywhere

Choose the integration path that matches your stack. All options pull the same real-time data that powers shootershub.fortneyengineering.com.

iFrame (fastest)

Hosted by The Shooters Hub. Set query parameters to pre-filter by type, sort order, or view.

<iframe
  src="https://shootershub.fortneyengineering.com/match-finder?type=NRL22&sort=dateAsc"
  loading="lazy"
  style="width:100%;height:720px;border:0;border-radius:18px;"
  allow="clipboard-read; clipboard-write; geolocation"
></iframe>

No API key required. The iframe automatically stays in sync with the latest public data.

Script widget

Lightweight helper that mounts the finder into any container. Configure filters through a JSON data attribute.

<div class="shmf" data-sh-config='{"type":["NRL22"],"view":"map","radius":150,"center":{"lat":39.95,"lng":-75.16}}'></div>
<script src="https://shootershub.fortneyengineering.com/embed/match-finder.js" defer></script>

Include `type`, `view`, `radius`, and `center` (lat/lng) keys inside `data-sh-config`. The script can be self-hosted via the `build/match-finder.js` artifact.

WordPress plugin

Download the zip from the `FortneyManufacturingLLC/shooters-hub-wp` releases and drop the shortcode or block into any page.

[shooters_hub_match_finder type="NRL22" view="map" radius="200"]

The plugin registers both a shortcode and a “Shooters Hub: Match Finder” block, proxies API calls via your token, and can auto-update from GitHub.

HTTP API overview

Core read endpoints stay open so clubs, series, and third-party developers can build without friction. For write access or advanced reports, request a scoped API key.

GET/api/matches

Query public matches

Returns match metadata straight from Firestore. Supports location filters, series/season logic, and results are cached for 30 seconds.

typeCSV

Filter by disciplines (e.g., `NRL22`, `PRS Rimfire`).

fromYYYY-MM-DD

Inclusive start date for results.

toYYYY-MM-DD

Inclusive end date for results.

lat & lngnumber

Center the search by coordinates. Combine with `radius` in miles.

zipstring

Server-side postal lookup. Provide `country` (ISO code) when outside the US.

seriesCSV

Filter by series IDs or slugs. Use `seriesMode=and` to require all values.

seasonsCSV

Filter by season IDs. Supports `seasonsMode=and` like series.

sortdateAsc | dateDesc | nameAsc | nameDesc | distance

Sort order for the response. Distance sorting requires `lat` and `lng`.

limit1-1000

Max results to return (defaults to 200, caps at 1000).

Example

curl "https://shootershub.fortneyengineering.com/api/matches?type=NRL22&from=2025-01-01&lat=43.12&lng=-90.45&radius=150&limit=50"

Response

{
  "items": [
    {
      "id": "match_abcd1234",
      "name": "Driftless Rimfire - January 2025",
      "date": "2025-01-18",
      "type": "NRL22",
      "matchTier": "regional",
      "status": "Open",
      "location": { "lat": 43.12, "lng": -90.45, "name": "Driftless Rimfire Club" },
      "distanceMi": 84.2,
      "seriesIds": ["driftless-league"],
      "seasonIds": ["2025-midwest-rimfire"],
      "startTime": "08:30"
    }
  ],
  "nextCursor": ""
}
GET/api/matches/bulk

Batch hydrate matches

Look up many matches by document ID in a single call. Provide up to 200 IDs via `ids` and use an API key with the `matches.read` scope (or match-scoped variant) to include unpublished metadata you already have access to.

idsCSV | JSON array • required

Comma/newline separated list of match IDs. Works as `?ids=match_a,match_b` for GET or `{ "ids": ["match_a", "match_b"] }` in POST. Duplicates and private matches are skipped.

Example

curl "https://shootershub.fortneyengineering.com/api/<your-api-key>/matches/bulk?ids=match_abcd1234,match_wxyz5678"

Response

{
  "items": [
    {
      "id": "match_abcd1234",
      "name": "Driftless Rimfire - January 2025",
      "date": "2025-01-18",
      "endDate": "2025-01-19",
      "type": "NRL22",
      "matchTier": "regional",
      "status": "Open",
      "clubId": "club_driftless",
      "location": { "name": "Driftless Rimfire Club", "lat": 43.12, "lng": -90.45 },
      "seriesIds": ["driftless-league"],
      "seasonIds": ["2025-midwest-rimfire"],
      "startTime": "08:30",
      "documents": { "cofUrl": "https://example.com/driftless-jan-2025-cof.pdf" }
    }
  ]
}
GET/api/clubs/bulk

Batch hydrate clubs

Returns sanitized club profiles, PractiScore metadata, and linked entities for IDs you already manage. Requires an API key carrying `clubs.read` (or a club-scoped variant) and respects the same rate limits shown in your key manager.

idsCSV | JSON array • required

List of club document IDs. Submit as `?ids=club_alpha,club_beta` or JSON `{ "ids": [...] }`. Only public or administrator-visible clubs are returned.

Example

curl "https://shootershub.fortneyengineering.com/api/<your-api-key>/clubs/bulk?ids=club_driftless"

Response

{
  "items": [
    {
      "id": "club_driftless",
      "name": "Driftless Rimfire Club",
      "bio": "Volunteer-led rimfire club serving southwest Wisconsin.",
      "website": "https://driftlessrimfire.example.com",
      "location": { "name": "Driftless Rimfire Range", "lat": 43.12, "lng": -90.45 },
      "seasonIds": ["2025-midwest-rimfire"],
      "matchIds": ["match_abcd1234"],
      "practiscoreSlug": "driftless-rimfire",
      "practiscoreLinks": { "website": "https://practiscore.com/clubs/driftless-rimfire" },
      "public": true
    }
  ]
}
GET/api/series/bulk

Batch hydrate series

Fetches public series with linked clubs, sponsor IDs, and configured defaults. Use an API key that includes `series.read` or a series-scoped grant to mirror the same information available in the dashboard.

idsCSV | JSON array • required

Comma/newline separated series IDs or a JSON array body. Responses keep the ordering you pass in and omit non-public series.

Example

curl "https://shootershub.fortneyengineering.com/api/<your-api-key>/series/bulk?ids=driftless-league"

Response

{
  "items": [
    {
      "id": "driftless-league",
      "name": "Driftless Rimfire League",
      "shortName": "Driftless League",
      "slug": "driftless-rimfire-league",
      "description": "Regional rimfire series pairing monthly club matches into a points race.",
      "associationId": "assoc_midwest_rimfire",
      "logoURL": "https://cdn.shootershub.app/series/driftless/logo.png",
      "clubIds": ["club_driftless"],
      "seasonIds": ["2025-midwest-rimfire"],
      "links": { "website": "https://driftlessrimfire.example.com" },
      "public": true
    }
  ]
}
GET/api/seasons/bulk

Batch hydrate seasons

Hydrate season schedules, leaderboard rules, and linked matches in one request. Requires a key with `seasons.read` (or a season-specific grant) and only surfaces data visible to that scope.

idsCSV | JSON array • required

Ordered list of season IDs to fetch. Accepts CSV, newline-delimited text, or JSON bodies. Private or hidden seasons are skipped automatically.

Example

curl "https://shootershub.fortneyengineering.com/api/<your-api-key>/seasons/bulk?ids=2025-midwest-rimfire"

Response

{
  "items": [
    {
      "id": "2025-midwest-rimfire",
      "name": "2025 Midwest Rimfire Season",
      "seriesId": "driftless-league",
      "startDate": "2025-01-01",
      "endDate": "2025-12-31",
      "leaderboardRules": {
        "metric": "points",
        "selection": { "mode": "bestOf", "topN": 8 },
        "filters": { "allowedDivisions": ["Open", "Young Guns"] }
      },
      "matches": [
        { "matchId": "match_abcd1234", "include": true, "countsForScoring": true, "date": "2025-01-18" }
      ],
      "classes": ["Open", "Base"],
      "divisions": ["Young Guns", "Ladies"],
      "public": true
    }
  ]
}
GET/api/users/bulk

Batch hydrate shooter profiles

Returns public shooter profiles—including bios, social links, and opt-in home locations—for IDs you already have permission to view. Authenticate with an API key that carries the same read scopes you use in the dashboard.

idsCSV | JSON array • required

Provide user IDs as `?ids=user_123,user_456` or `{ "ids": ["user_123", "user_456"] }`. Profiles marked private are filtered out automatically.

Example

curl "https://shootershub.fortneyengineering.com/api/<your-api-key>/users/bulk?ids=user_abc123"

Response

{
  "items": [
    {
      "id": "user_abc123",
      "displayName": "Jamie Marks",
      "username": "jamie_marks",
      "bio": "Rimfire competitor and match director.",
      "photoURL": "https://cdn.shootershub.app/profiles/jamie.png",
      "homeLocation": { "name": "Madison, WI", "lat": 43.07, "lng": -89.4 },
      "gear": {
        "binos": "Leupold BX-4",
        "firearms": {
          "rimfire": [
            { "optic": "Vortex Strike Eagle 3-18x", "chassis": "MDT ACC", "action": "Vudoo V-22" }
          ]
        }
      },
      "links": { "website": "https://jamieshooting.example.com" },
      "socials": { "instagram": "https://instagram.com/jamie.rimfire" }
    }
  ]
}
GET/api/olc/search

Retrieve tile coverage

Generates Open Location Code (OLC) tiles for the match finder. Use `mode=circle` with a lat/lng/radius or `mode=list` with explicit tile IDs. Responses include caching headers and ETags for conditional requests.

modecircle | list • required

Whether to compute tiles from a circle or fetch specific tiles.

lat & lngnumber

Circle center coordinates (required for `mode=circle`).

radiusMinumber

Circle radius in miles.

monthsCSV YYYY-MM

Limit tiles to specific months (defaults to the current month).

tilesCSV tileId

List of `OLC4:YYYY-MM` tile IDs to fetch when `mode=list`.

disciplines | subDiscipline | tiersCSV

Optional server-side filters applied to tile contents.

havetileId@ISO, ...

Send cached tile timestamps to receive `notModified` entries instead of payloads.

Example

curl "https://shootershub.fortneyengineering.com/api/olc/search?mode=circle&lat=43.12&lng=-90.45&radiusMi=150&months=2025-01,2025-02"

Response

{
  "tiles": [
    {
      "tileId": "86JQ:2025-01",
      "olc4": "86JQ",
      "yyyymm": "2025-01",
      "updatedAt": "2024-12-10T04:12:11.000Z",
      "count": 6,
      "items": [
        { "id": "match_abcd1234", "title": "Driftless Rimfire - January 2025", "lat": 43.12, "lng": -90.45, "date": "2025-01-18" }
      ]
    }
  ],
  "notModified": [],
  "missing": [],
  "fulfilledTiles": ["86JQ:2025-01"]
}
POST/api/olc/tiles

Batch fetch tile details

Fetches specific tiles in bulk, honoring cache validation via `have`. Helpful when you already know which tiles are relevant and need the raw matches inside each tile.

tileIdsstring[] • required

Array of tile IDs such as `86JQ:2025-01`.

haveArray<{ tileId, updatedAt }>

Client cache metadata to skip unchanged tiles.

disciplines | subDiscipline | tiersstring[]

Optional filters applied server-side before returning tile contents.

Example

curl -X POST https://shootershub.fortneyengineering.com/api/olc/tiles \
  -H "Content-Type: application/json" \
  -d '{"tileIds":["86JQ:2025-01","86JR:2025-01"]}'

Response

{
  "tiles": [
    { "tileId": "86JQ:2025-01", "count": 6, "items": [/* ... */] }
  ],
  "notModified": [],
  "missing": [],
  "fulfilledTiles": ["86JQ:2025-01"]
}
Live API playground

Test queries without leaving the page

The playground uses the same `apiUrl()` helper as the web app. Configure filters, send a request, and inspect the JSON response instantly.

If latitude/longitude are blank we will resolve the ZIP (default country = US).

Need private endpoints? Authenticate with an API key—send it as the x-api-key header or rewrite the path to /api/<your-api-key>/… before calling the Functions origin.

Request URL (replace <your-api-key>)

https://shootershub.fortneyengineering.com/api/<your-api-key>/matches?type=NRL22&limit=50&sort=dateAsc&radius=150

Production calls can send the same value as an x-api-key header instead of rewriting the path. Both styles consume the per-minute and per-day limits you configure in the API key manager, while this playground still hits our first-party host without adding a key so you can experiment safely.

cURL

curl "https://shootershub.fortneyengineering.com/api/<your-api-key>/matches?type=NRL22&limit=50&sort=dateAsc&radius=150"

Response

// Run the query to preview JSON output.

Authentication & scopes

GET /api/matches and every /api/olc/* route stay public. Private lookups require an API key—either send it as an x-api-key header or rewrite the path to /api/<your-api-key>/…. Functions remove the key segment before hitting the handler. Create, scope, and rotate keys from the API key manager to keep each integration isolated.

Rate limiting & caching

Public endpoints ship with Cache-Control headers (matches: 30s; OLC tiles: 5 min) plus ETags. Respect If-None-Match and watch the per-minute / per-day quotas displayed alongside each key in the manager—header and path-style authentication both draw from the same limits.

Local development

When running the Next.js app locally, set `NEXT_PUBLIC_API_BASE` to your Functions origin (or `/api` to proxy through the Next API route). The `apiUrl()` helper in `@/utils/api` builds the correct path for both environments.

Shipping something ambitious?

We partner with match directors, clubs, and integrators to launch custom experiences. Share your roadmap and we will line up the right APIs, caches, and billing model.

Developer Hub — The Shooters Hub