CourtTideAtlanta

Connect your AI

Connect Muse or GrokBot to find courts, find hitting partners and log your matches. There’s nothing to install.

  • “Find me a court near home Saturday morning.”
  • “Who’s looking to hit this week at my level?”
  • “Log my match: beat Sam 6–3, 6–4.”

Set it up

  1. Paste it into your AI.
  2. Let it finish. It either has you sign in to CourtTide in its own browser, or tells you how to create a key and asks for it in a secure prompt.
Muse

Paste the prompt into a Muse chat. Muse creates a custom connector and opens a secure prompt for your key. Create your key on the API keys page, then enter Bearer, a space and the key. Muse may ask before some actions; choose Allow.

GrokBot

Paste the prompt into GrokBot. If it has its own browser, it opens CourtTide, has you sign in with your email code, then creates and stores the key itself. Otherwise add a custom connector at grok.com/connectors: New Connector, Custom, then the URL under Connection details below, and complete its sign-in step with your key.

Connection details

For anyone doing it by hand. Create and manage keys on the API keys page; a key shows once. New keys start with all permissions (Admin). Narrow them if you want.

Endpoint
https://courttide.com/mcp
Transport
Streamable HTTP
Authentication header
Authorization: Bearer $COURTTIDE_API_KEY

Replace $COURTTIDE_API_KEY with your key. Keep it out of URLs, chats and logs.

To check the connection, call list_venues with {"venue_id":"bagley","limit":1}, then get_venue with {"venue_id":"bagley"}. Both read saved park data. Check isError in each result, since a tool can report an error even when HTTP succeeds. If a tool is missing, reconnect and compare with public capabilities.

Other assistants

Anything that can send a key in a header can connect. The ChatGPT and Claude apps can’t connect yet.

Hermes

Run the setup, then paste the key at the masked prompt. Never put the key in command arguments or config files.

hermes mcp add courttide --url https://courttide.com/mcp --auth header
hermes mcp test courttide

The first read in Hermes is mcp_courttide_list_venues. After changing permissions, run /reload-mcp or start a new chat. See the Hermes MCP guide for more.

Tools and permissions

48 tools, each checked against its own key permission. Every key allows 60 requests a minute. Changes to your account share 20 writes a minute and 500 a day.

Some things stay in the browser: age confirmation, gender disclosure, sign-in, photo upload and key management (a key can revoke itself). You can’t delete your account through your AI.

All 48 tools

  • list_venuesavailability:read

    List compact venue/court identities with bounded pagination, an optional venue filter and conjunctive surface/lights/access/booking/hours/cost facts; selected filters require known matches.

    60 requests/min/key

  • get_venueavailability:read

    Read complete saved catalog facts, qualifications and provenance for one venue; dated observations use get_historical_schedule.

    60 requests/min/key; one venue/read

  • search_availabilityavailability:read

    Search saved schedule evidence using full-duration rules.

    100 searches/account/UTC day; 60 requests/min/key

  • get_historical_scheduleavailability:read

    Read bounded historical schedule evidence.

    60 requests/min/key; 100 observations/page

  • submit_feedbackfeedback:write

    Submit an idempotent private report or court suggestion.

    10/day for each report, suggestion, and contribution; contributions max 3/park/day; shared 20 writes/min and 500/day/account

  • list_feedbackfeedback:read

    List only this account's private feedback.

    60 requests/min/key; 50 reports/page

  • get_feedbackfeedback:read

    Read one account-owned private feedback report.

    60 requests/min/key

  • get_account_limitsany authenticated key (no additional scope)

    Read this account's remaining action budgets and reset times.

    Read-only own counters; 60 requests/min/key

  • list_saved_courtscourts:read

    List this owner's saved parks.

    60 requests/min/key; at most five saved parks

  • save_courtcourts:write

    Save one published park.

    At most five saved parks; shared 20 writes/min and 500/day/account

  • remove_saved_courtcourts:write

    Remove one saved park.

    Shared 20 writes/min and 500/day/account

  • get_starting_pointlocation:write

    Read this owner's starting point.

    60 requests/min/key; private owner-only coordinates

  • geocode_locationlocation:write

    Look up an address or label coordinates without saving; reverse lookup retains submitted coordinates.

    60 geocode attempts/account/UTC day; 60 requests/min/key; shared Geoapify project budget

  • set_starting_pointlocation:write

    Set a starting point from coordinates.

    10 changes/account/UTC day plus shared 20 writes/min and 500/day

  • clear_starting_pointlocation:write

    Clear this owner's starting point.

    10 changes/account/UTC day plus shared 20 writes/min and 500/day

  • get_my_gameprofile:write

    Read this owner's safe game profile.

    60 requests/min/key

  • update_my_gameprofile:write

    Update supported game fields and optional Strava/Instagram profile links; gender needs current browser consent.

    30 saves/account/UTC day plus shared 20 writes/min and 500/day

  • set_listed_parksprofile:write

    Set this owner's listed parks.

    20 changes/account/UTC day; at most three listed saved parks

  • set_open_to_hitprofile:write

    Turn this owner's Open to hit listing off or on.

    10 changes/account/UTC day; enabling requires current browser consent and eligibility

  • list_matchesmatches:read

    Read this owner's match history, filters and full-history nonsample summary.

    60 requests/min/key; at most 50 matches/page

  • create_matchmatches:write

    Log a match with an optional catalog park or club; identical UUID retries are free.

    100 changed match mutations/account/UTC day plus shared write/key limits

  • update_matchmatches:write

    Edit an owned match with stale-edit protection.

    100 changed match mutations/account/UTC day plus shared write/key limits

  • delete_matchmatches:write

    Delete an owned match; absent matches are a free no-op.

    100 changed match mutations/account/UTC day plus shared write/key limits

  • list_visitsvisits:write

    List public parks played, shown on the member profile.

    60 requests/min/key; current public parks only

  • mark_visitvisits:write

    Mark a public park as played, without dates or notes.

    20 new marks/account/UTC day; one per public park; shared write limits

  • remove_visitvisits:write

    Unmark a public park by venue_id.

    Shared 20 writes/min and 500/day/account; no-op deletes are free

  • revoke_calling_keyauthenticated key identity (no separate scope)

    Revoke the API key used for this call.

    Self-revokes only the calling key; shared 20 writes/min and 500/day

  • get_player_profileplayers:read

    Read a member profile without contact details.

    60 requests/min/key; existing viewer-bound handle required

  • search_playersplayers:read

    Read player and post counts for selected parks.

    60 requests/min/key; at most 50 parks/key/query

  • list_players_at_parkplayers:read

    Read one park's reciprocal standing list.

    40 new views/rolling 24 hours and 400 pages/account/UTC day

  • read_hit_boardplayers:read

    Read posts visible to this owner.

    2000 reads/account/UTC day

  • create_hit_postboard:write

    Create a bounded time-to-hit post.

    20 creates/account/UTC day; max five open and one per local play date, within a seven-day board window

  • get_hit_postplayers:read

    Read one explicitly published post under current profile publication and safety policy.

    60 requests/min/key; one public open post by UUID

  • set_hit_post_publicationboard:publish

    Explicitly publish or unpublish an owned post; never publishes its profile.

    20 changed existing-post publication operations/account/UTC day; shared20 writes/min and500/day/account

  • list_my_hit_postsboard:write

    List this owner's open hit posts.

    60 requests/min/key

  • withdraw_hit_postboard:write

    Withdraw one of this owner's posts.

    Shared 20 writes/min and 500/day/account

  • send_hit_requestrequests:write

    Send a hit request to an eligible player.

    10/rolling24h for every account; no refunds; one open/pair;30-day decline/expiry/accepted/ended cooldown excludes sender cancellation; existing sender/recipient/global mail budgets;14-day horizon

  • list_hit_requestsrequests:read

    List this owner's sent and received requests.

    60 requests/min/key; owner-scoped 50 per page; reads never acknowledge

  • get_hit_request_summaryrequests:read

    Read this owner's pending requests and unseen acceptances.

    60 requests/min/key; read never acknowledges

  • cancel_hit_requestrequests:write

    Cancel this owner's pending sent request.

    20 changes/account/UTC day plus shared write limits; cancellation is not a refund

  • mark_hit_request_seenrequests:write

    Explicitly mark this owner's acceptance notice seen.

    100 changes/account/UTC day plus shared write limits

  • answer_hit_requestrequests:write

    Accept with explicit contact fields or decline a request.

    Incoming live request; acceptance shares only explicit fields; sender mail 6/day, recipient 2/24h and 6/7d, global 90/day and 30 hit/day

  • mark_request_playedrequests:write

    Record that both players met.

    Once per participant for an eligible accepted request; shared write limits

  • block_social_targetany social scope

    Block a player, post owner or request participant.

    50 combined block/unblock changes/account/UTC day

  • list_blocked_playersany social scope

    List this owner's blocked-player records.

    60 requests/min/key; owner-only pages of 50

  • unblock_playerany social scope

    Remove one of this owner's block records.

    50 combined block/unblock changes/account/UTC day

  • set_park_muteany social scope

    Mute or unmute hit requests from a park.

    20 changes/account/UTC day

  • report_social_targetany social scope

    Report a player, post or request for review.

    10 reports/account/UTC day; one open report per target

Log a match

Add matches:read and matches:write to a key to let the agent manage your Match Tracker, plus availability:read to pick the park.

  1. Find the venue with list_venues or get_venue. Use its venue.id and a returned venue.courts[].id, not a court label.
  2. Read list_matches for that month and compare date, format, opponents and scores before adding a record. Follow next_cursor with the same filters.
  3. Once you approve the match, send create_match with one fresh UUID and keep it, unchanged, for any retry.
  4. Read the log back and find the returned match.id. created:false means that UUID already holds the same match. The same UUID with different fields returns match_conflict (409).

These are tool calls in order, not a batch endpoint. The create writes a real record. The example uses Bagley, Court 1.

[
  {
    "name": "list_venues",
    "arguments": {
      "venue_id": "bagley",
      "limit": 1
    }
  },
  {
    "name": "get_venue",
    "arguments": {
      "venue_id": "bagley"
    }
  },
  {
    "name": "list_matches",
    "arguments": {
      "year": 2026,
      "month": 11,
      "limit": 50
    }
  },
  {
    "name": "create_match",
    "arguments": {
      "id": "24e6fa9a-40b7-4bde-87a9-34f09941d93f",
      "date": "2026-11-02",
      "format": "singles",
      "league": null,
      "opponents": [
        "Example Opponent"
      ],
      "partner": null,
      "venue_id": "bagley",
      "court_id": "00000000-0000-4000-8000-000000007245",
      "hide_park": false,
      "status": "completed",
      "season": null,
      "notes": null,
      "sample": false,
      "result": "unknown",
      "scores": []
    }
  },
  {
    "name": "list_matches",
    "arguments": {
      "year": 2026,
      "month": 11,
      "limit": 50
    }
  }
]

Status is completed (default), retired, walkover or forfeit. Season allows 80 characters and private notes 1,000. For an unknown location, send venue_id:null and court_id:null. Sample matches stay in your own log and are left out of shared histories. Notes are only visible to you. To edit, send the full current fields with the exact expected_updated_at. Match changes share 100 a day per account; exact retries are free.

Limits

401
Missing, invalid or revoked key. Check the stored key and do not echo it.
400 / 403
Invalid input, missing permission or a forbidden action. Read the error code and fix the request.
429
Stop and back off, and honor Retry-After. Keys share account limits, so rotating keys does not raise them.
503 or unknown
A service or source may be unavailable. Never read this as a booked court.

Court times come from each park’s public booking page. CourtTide never books, holds or pays for a court.

For developers

Python client

With mcp==2.1.1 and httpx2, set COURTTIDE_API_KEY privately. This reads one saved park and changes nothing.

import asyncio
import json
import os

import httpx2
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

async def main():
    async with httpx2.AsyncClient(
        headers={"Authorization": "Bearer " + os.environ["COURTTIDE_API_KEY"]},
        follow_redirects=False, timeout=30,
    ) as http:
        async with streamable_http_client(
            "https://courttide.com/mcp", http_client=http
        ) as (read, write):
            async with ClientSession(read, write, read_timeout_seconds=30) as session:
                await session.initialize()
                tools = await session.list_tools()
                assert any(tool.name == "list_venues" for tool in tools.tools)
                result = await session.call_tool(
                    "list_venues", {"venue_id": "bagley", "limit": 1}
                )
                if result.is_error:
                    raise RuntimeError("CourtTide read failed; inspect privately")
                # Keep this account-scoped response private.
                print(json.dumps(result.structured_content))

asyncio.run(main())
Direct HTTP API

The same service over HTTP, independent of MCP. Send Authorization: Bearer $COURTTIDE_API_KEY and keep the key out of URLs. Each key is limited to its selected permissions. Set the key in your shell first.

curl --silent --show-error --fail-with-body \
  "https://courttide.com/api/v1/capabilities" \
  --header "Authorization: Bearer $COURTTIDE_API_KEY"

Check schema_version, features, enabled_venues and supported_filters, and keep that response private. No key is needed for public capabilities, the public OpenAPI schema or the park catalog, which holds no openings. Full OpenAPI and runtime capabilities need a key.

API routes
MethodPathPermissionPurpose
POST/api/v1/agent/actionsPer-action scopeCall one owner action, the same as its MCP tool, with strict input
POST/api/v1/feedbackfeedback:writeSubmit a private report
GET/api/v1/feedbackfeedback:readList your own reports
GET/api/v1/feedback/{id}feedback:readRead one of your own reports
GET/api/v1/venuesavailability:readCompact catalog: limit 20 (max 50), cursor, exact venue_id filter
GET/api/v1/venues/{venueId}availability:readComplete saved facts and coverage for one venue
POST/api/v1/availability/searchavailability:readFull-duration court search
GET/api/v1/availability/historyavailability:readPast observations; needs a date or capture range

Venue lists return 20 rows by default (1 to 50), ordered by venue ID. Pass next_cursor back as cursor and keep the same venue_id filter while paging. Venue and court facts.fact_summary carry the Lights, Surface, Hours and Cost shown on the site. Each entry has its state, exact fact IDs and original dates. Facts are definitive until an accepted correction changes them; unknown values are null. Stored review metadata is private. Identified court surfaces cover only their listed IDs and establish no physical count; park hours are not court access hours.

Search saved times

Peachtree Hills Park, Court 1, on 2026-11-06 at 10 AM for 120 minutes plus 15 minutes of warm-up. The whole session must fit one continuous open interval. It reads saved data and requests no drive estimate or refresh.

curl --silent --show-error --fail-with-body --request POST \
  "https://courttide.com/api/v1/availability/search" \
  --header "Authorization: Bearer $COURTTIDE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
  "venue_ids": [
    "peachtree-hills"
  ],
  "court_ids": [
    "00000000-0000-4000-8000-000000007535"
  ],
  "date": "2026-11-06",
  "earliest_start": "10:00",
  "latest_start": "10:00",
  "duration_minutes": 120,
  "warmup_minutes": 15,
  "constraints": {
    "daylight": false
  },
  "include_drive_estimates": false
}'

Dates run from today through 90 days ahead, in Atlanta time, with five-minute starts, sessions of 15 to 240 minutes and 0 to 60 minutes of warm-up. Daylight and lighting inform the player but do not filter times. Match Maker posts use a separate window of today through six days ahead.

saved_schedule
The latest complete saved read for each court and day, with its original observation time, intervals, observed scope and options. A later failed read does not erase it. Never combine older openings into it.
matches
Sessions in that read. Check each observed_at. Starts whose warm-up has begun are left out.
last_seen_open
Earlier observations only, not current availability.
alternatives / excluded_or_unknown
Shorter sessions, failed constraints and missing facts. Unknown price or drive time never means zero, and a missing schedule never means fully booked.

Searches use the ordinary account read allowance. Send the query directly; each retry is another read of the saved observations.

Court feedback

submit_feedback, list_feedback and get_feedback use the same service as Send feedback, with feedback:write or feedback:read on the key. Reports are private proposals for review. They never publish a fact by themselves, and API keys cannot review them.

Send a new UUID in Idempotency-Key and keep it with the exact report for any retry. A new report returns 201, a matching retry returns 200, and the same UUID with changed content returns 409. Do not submit keys, sign-in links, payment details or private home locations.

curl --fail-with-body 'https://courttide.com/api/v1/feedback' \
  -H "Authorization: Bearer $COURTTIDE_FEEDBACK_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $COURTTIDE_REPORT_UUID" \
  --data '{"kind":"incorrect_fact","venue_id":"bagley","message":"Please review the posted venue hours.","source_url":"https://example.test/public-court"}'

curl --fail-with-body 'https://courttide.com/api/v1/feedback' \
  -H "Authorization: Bearer $COURTTIDE_FEEDBACK_KEY"

curl --fail-with-body 'https://courttide.com/api/v1/feedback/'"$COURTTIDE_REPORT_ID" \
  -H "Authorization: Bearer $COURTTIDE_FEEDBACK_KEY"

kind is incorrect_fact, court_details, suggest_court or feature_request. A free-text message runs 1 to 2,000 characters. venue_id is a catalog slug, court_id a UUID and source_url a public HTTPS link. Lists return reports and next_cursor, passed back as cursor, and show only your own account; another account’s report returns 404.

A structured court_details report needs a selected venue_id, and suggest_court needs a selected park or details.park.name. Add at least one explicit answer or note per section; unsure counts. The details limit is 8 KB. See the OpenAPI schema for every section.

{
  "kind": "court_details",
  "venue_id": "bagley",
  "details": {
    "lights": {
      "notes": "Please review the winter lighting schedule."
    }
  }
}

Follow status (received, under_review, resolved or dismissed), review_note and reviewed_at. Resolved can mean a correction or a decision to change nothing, so read the note.

Support

Stuck, or a tool missing from your AI? support@courttide.com