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
- Paste it into your AI.
- 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 courttideThe 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:readList 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:readRead 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:readSearch saved schedule evidence using full-duration rules.
100 searches/account/UTC day; 60 requests/min/key
get_historical_scheduleavailability:readRead bounded historical schedule evidence.
60 requests/min/key; 100 observations/page
submit_feedbackfeedback:writeSubmit 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:readList only this account's private feedback.
60 requests/min/key; 50 reports/page
get_feedbackfeedback:readRead 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:readList this owner's saved parks.
60 requests/min/key; at most five saved parks
save_courtcourts:writeSave one published park.
At most five saved parks; shared 20 writes/min and 500/day/account
remove_saved_courtcourts:writeRemove one saved park.
Shared 20 writes/min and 500/day/account
get_starting_pointlocation:writeRead this owner's starting point.
60 requests/min/key; private owner-only coordinates
geocode_locationlocation:writeLook 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:writeSet a starting point from coordinates.
10 changes/account/UTC day plus shared 20 writes/min and 500/day
clear_starting_pointlocation:writeClear this owner's starting point.
10 changes/account/UTC day plus shared 20 writes/min and 500/day
get_my_gameprofile:writeRead this owner's safe game profile.
60 requests/min/key
update_my_gameprofile:writeUpdate 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:writeSet this owner's listed parks.
20 changes/account/UTC day; at most three listed saved parks
set_open_to_hitprofile:writeTurn this owner's Open to hit listing off or on.
10 changes/account/UTC day; enabling requires current browser consent and eligibility
list_matchesmatches:readRead this owner's match history, filters and full-history nonsample summary.
60 requests/min/key; at most 50 matches/page
create_matchmatches:writeLog 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:writeEdit an owned match with stale-edit protection.
100 changed match mutations/account/UTC day plus shared write/key limits
delete_matchmatches:writeDelete an owned match; absent matches are a free no-op.
100 changed match mutations/account/UTC day plus shared write/key limits
list_visitsvisits:writeList public parks played, shown on the member profile.
60 requests/min/key; current public parks only
mark_visitvisits:writeMark a public park as played, without dates or notes.
20 new marks/account/UTC day; one per public park; shared write limits
remove_visitvisits:writeUnmark 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:readRead a member profile without contact details.
60 requests/min/key; existing viewer-bound handle required
search_playersplayers:readRead player and post counts for selected parks.
60 requests/min/key; at most 50 parks/key/query
list_players_at_parkplayers:readRead one park's reciprocal standing list.
40 new views/rolling 24 hours and 400 pages/account/UTC day
read_hit_boardplayers:readRead posts visible to this owner.
2000 reads/account/UTC day
create_hit_postboard:writeCreate 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:readRead 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:publishExplicitly 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:writeList this owner's open hit posts.
60 requests/min/key
withdraw_hit_postboard:writeWithdraw one of this owner's posts.
Shared 20 writes/min and 500/day/account
send_hit_requestrequests:writeSend 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:readList this owner's sent and received requests.
60 requests/min/key; owner-scoped 50 per page; reads never acknowledge
get_hit_request_summaryrequests:readRead this owner's pending requests and unseen acceptances.
60 requests/min/key; read never acknowledges
cancel_hit_requestrequests:writeCancel this owner's pending sent request.
20 changes/account/UTC day plus shared write limits; cancellation is not a refund
mark_hit_request_seenrequests:writeExplicitly mark this owner's acceptance notice seen.
100 changes/account/UTC day plus shared write limits
answer_hit_requestrequests:writeAccept 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:writeRecord that both players met.
Once per participant for an eligible accepted request; shared write limits
block_social_targetany social scopeBlock a player, post owner or request participant.
50 combined block/unblock changes/account/UTC day
list_blocked_playersany social scopeList this owner's blocked-player records.
60 requests/min/key; owner-only pages of 50
unblock_playerany social scopeRemove one of this owner's block records.
50 combined block/unblock changes/account/UTC day
set_park_muteany social scopeMute or unmute hit requests from a park.
20 changes/account/UTC day
report_social_targetany social scopeReport 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.
- Find the venue with
list_venuesorget_venue. Use itsvenue.idand a returnedvenue.courts[].id, not a court label. - Read
list_matchesfor that month and compare date, format, opponents and scores before adding a record. Follownext_cursorwith the same filters. - Once you approve the match, send
create_matchwith one fresh UUID and keep it, unchanged, for any retry. - Read the log back and find the returned
match.id.created:falsemeans that UUID already holds the same match. The same UUID with different fields returnsmatch_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.
| Method | Path | Permission | Purpose |
|---|---|---|---|
| POST | /api/v1/agent/actions | Per-action scope | Call one owner action, the same as its MCP tool, with strict input |
| POST | /api/v1/feedback | feedback:write | Submit a private report |
| GET | /api/v1/feedback | feedback:read | List your own reports |
| GET | /api/v1/feedback/{id} | feedback:read | Read one of your own reports |
| GET | /api/v1/venues | availability:read | Compact catalog: limit 20 (max 50), cursor, exact venue_id filter |
| GET | /api/v1/venues/{venueId} | availability:read | Complete saved facts and coverage for one venue |
| POST | /api/v1/availability/search | availability:read | Full-duration court search |
| GET | /api/v1/availability/history | availability:read | Past 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