# SplitClub integration guide (full text) > SplitClub is the payment system for group bookings. A venue tells SplitClub what it sells, in one typed > vocabulary; SplitClub runs the group checkout: the organizer picks their people, each person's share is held on > their own card, and the venue is paid once the whole party has paid. Partners never handle a guest's card and > never compute shares. This file is the whole guide as plain text, for people and for AI agents. Version 1. Canonical address: https://splitclubapp.com/developers/llms-full.txt Machine-readable companions: - JSON Schemas (the single source of truth): https://splitclubapp.com/schemas/v1/ (index: /schemas/v1/index.json) - OpenAPI 3.1, generated from the schemas: https://splitclubapp.com/openapi.json - Provider mappings: https://splitclubapp.com/schemas/v1/mappings/.json - MCP server for AI agents: https://splitclubapp.com/api/mcp (Streamable HTTP, authenticated with your key) - Short index: https://splitclubapp.com/llms.txt - People: https://splitclubapp.com/developers and partners@splitclubapp.com Contents 1. The principle 2. Keys, modes and scopes 3. Money, time and ids 4. The objects 5. Requests: auth, idempotency, limits 6. Offerings: telling SplitClub what you sell 7. Pay with SplitClub: the checkout flow 8. Webhooks and signature verification 9. Errors 10. Provider mappings 11. The MCP server 12. Walkthrough: hook up a golf resort, end to end 13. The same pattern elsewhere 14. Security 15. Versioning and contact ## 1. The principle SplitClub is the payment system. You describe what you sell: Offerings (a tee time, a table, a cabana, a treatment, a boat day), each priced by PriceTiers and bookable per its Availability, under a Policy. When a group books, your server creates a Checkout for the total. SplitClub collects it from the group: the organizer chooses who is coming (SplitClub friends, a group chat, phone numbers or a link), the total is split, and each person pays their own share as a card hold. Nothing is captured until every share is held. Then SplitClub captures every hold at once, marks the checkout completed, signs a webhook to you, and pays the venue out through its Stripe Connect account. What you never do: touch a guest's card, compute shares, chase late payers, or refund individuals by hand. What SplitClub never does: change your price. The amount comes only from your server, with your secret key. ## 2. Keys, modes and scopes Every key belongs to one venue. sc_test_<32> secret key, test mode. Self-serve. Test cards only; never touches live money. sc_live_<32> secret key, live mode. Issued by SplitClub after the venue is verified (domain or work-email code, Stripe Connect KYC complete). pk_test_<32> publishable key, test mode. Goes in the browser; can only open checkouts your server created. pk_live_<32> publishable key, live mode. Keys issued before v2 look like sc_live_ followed by 48 hex characters and keep working, with every scope. A sandbox key (from the Partner console, before SplitClub links you to a venue) is an sc_test_ key for a test business: every offering, checkout and MCP tool works with it in test mode; connecting a venue system (/v1/integrations) and live keys wait until the venue is linked. Mode is fixed by the key: a checkout made with an sc_test_ key is a test checkout forever, uses Stripe test mode, and can never touch live money; a live key can never make a test object. Scopes (live keys carry the ones SplitClub grants; build with a test key): offerings:write offerings:read checkouts:write checkouts:read bookings:read analytics:read webhooks:write boats:write boats:read Secret keys work only server to server. Their responses carry no Access-Control-Allow-Origin header, so a browser can't use one. Store them as secrets; a leaked key is revoked on request and replaced. Keys can carry an expiry and an IP allowlist. ## 3. Money, time and ids Money is an integer in minor units plus a lowercase ISO 4217 currency. 14500 with "usd" is $145.00. 8875 is $88.75. 145 is $1.45, not $145. Never send 145.0 or "145.00". Convert decimal dollars with round(dollars * 100). Times are ISO 8601 with an offset: 2026-11-07T07:30:00-05:00. A time without an offset is refused. Local wall-clock times inside a tier (time_window) are "HH:MM" in the venue's timezone (Availability.timezone, an IANA name such as America/New_York). Every object you send carries your own external_id (1 to 128 characters, no spaces). Sending the same external_id again updates that object; it never makes a second one. Retries are always safe. ## 4. The objects All schemas: https://splitclubapp.com/schemas/v1/.json. Unknown fields are rejected, so a typo is an error rather than silently ignored. Below, * marks a required field. Offering (offering.json): something sold, repeatedly. Sent with its tiers, availability and policy in one upsert. external_id* your id kind* tee_time | table | cabana | daybed | spa_treatment | class | boat_day | ticketed_event | package | rental | ride | dining title* up to 120 characters tiers* 1 to 20 PriceTiers description up to 4,000 characters category a SplitClub category slug (GET /v1/me lists yours) media up to 10 { url (https), type: image|video, alt } availability an Availability policy a Policy booking_url https link to it on your site metadata up to 20 string keys of your own PriceTier (price_tier.json): how an offering is priced. amount* integer minor units currency* "usd" unit* per_person | per_group | per_hour | per_unit external_id your id for the tier (needed to override it per slot) label "Weekday", "Twilight", "GA", "VIP" min_party, max_party 1 to 500 days which weekdays it applies to: mon tue wed thu fri sat sun (every day when absent) time_window { from: "15:00", to: "19:00" } (HH:MM, 24-hour), local start times it applies to fees up to 10 { type: service|tax|gratuity|other, label, percent OR amount, included: true|false } included: true means the amount already contains it; false means it's added on top deposit { percent } or { amount } minimum_spend integer minor units, for tables and cabanas Availability (availability.json): when it can be bought. timezone IANA name slots up to 1,000 { starts_at*, ends_at, capacity, external_id, tier_overrides: [{ tier, amount }] } recurring up to 20 { rrule*, starts_at* (DTSTART), until (date), duration_minutes, capacity } rrule is RFC 5545, e.g. FREQ=DAILY;BYHOUR=7,8,9;BYMINUTE=0,10,20,30,40,50 blackout_dates dates, e.g. "2026-12-25" lead_time_minutes how long before a slot it can still be booked cutoff_minutes how long before a slot the party must be fully paid (the checkout's pay_by default) Policy (policy.json): the house rules. cancellation up to 10 { hours_before, refund_pct }: cancelling at least hours_before the start refunds refund_pct min_age 0 | 18 | 21 dress_code up to 200 characters arrival_window_minutes notes up to 2,000 characters Event (event.json): a dated instance, such as a night, a tournament or a show. POST /v1/experiences accepts it (and still accepts the original experience body). title*, starts_at*, ends_at, external_id, offering_external_id, description, category, capacity, tiers (PriceTiers, e.g. GA and VIP), on_sale { starts_at, ends_at }, media, policy, booking_url, metadata CheckoutCreate (checkout_create.json): the body of POST /v1/checkouts. amount* the whole party's total, integer minor units (at least 100) currency* "usd" party_size* 2 to 50 pay_by* ISO 8601; every share must be held by then, or the checkout expires and every hold is released description* up to 200 characters, shown to every guest return_url* https, on one of your registered origins external_booking_id* your booking's id; doubles as the idempotency key cancel_url https, on a registered origin offering_external_id the offering it's for metadata up to 20 string keys, echoed on every read and webhook Checkout (checkout.json): what SplitClub returns. id ("chk_…"), object "checkout", mode, status, amount, currency, party_size, pay_by, description, venue { id, name } (id is null for a console sandbox), external_booking_id, url ("https://checkout.splitclubapp.com/c/chk_…"), shares, funded_amount, fee_amount (SplitClub's fee) and net_amount (what the venue is paid: amount - fee_amount; both partner view only), created_at, completed_at, metadata status: open (created) → collecting (the organizer chose the party) → funded (every share held) → completed (captured; payout scheduled), or expired (pay_by passed; everything released) or canceled. Share (share.json): one person's part. Read-only. id ("shr_…"), amount, status (invited | held | captured | released | refunded | failed), is_organizer, display_name, held_at. A share never carries a phone number or an email. Boat (boat.json): one boat in a rental fleet, for boat-rental platforms and fleet operators. POST /v1/boats takes one, or { "boats": [...] } with up to 100, each upserted on external_id. Reviewed before SplitClub uses it; approved boats appear only in the boat days and tie-ups SplitClub runs, never on a map. external_id* your id name* up to 120 characters type* motor_yacht | sport_yacht | catamaran | sailboat | center_console | pontoon | jet_ski | other length_ft* 5 to 600 capacity* guests for a day trip, crew not counted captain* included | optional | required_extra | bareboat location* { city*, region, country (ISO, upper case), marina, address, lat + lng together } pricing* 1 to 20 rates for the whole boat: { duration_hours*, amount* (minor units), currency*, label, captain_included, fuel_included, days } photo_urls* 1 to 20 https URLs, in order; the first is the cover make, model, year, cabins, deposit_amount (minor units), deposit_note, fees_note, amenities (up to 40), description, booking_url, availability { ical_url, url, timezone, blocks [{ starts_at, ends_at, status }] }, source_platform (a slug: your platform's name, or "own"), source_url, active (false deactivates), metadata (20 string keys), extra (anything else; seen by reviewers only) A fleet is per boat: results[] gives each one's result (created | updated | unchanged | error, errors at /boats//…); valid boats are saved even when others aren't. status: pending → approved | rejected; inactive while "active" is false. A changed boat goes back to pending. WebhookEvent (webhook_event.json): what SplitClub POSTs to you. See section 8. Error (error.json): see section 9. ## 5. Requests: auth, idempotency, limits Base URL: https://splitclubapp.com/api Auth: Authorization: Bearer sc_test_… (or sc_live_…) on every request, from your server. Bodies: JSON, at most 64 KB (1 MB for POST /v1/boats). Idempotency: every POST accepts an Idempotency-Key header; a replay returns the first result. external_id (offerings, events) and external_booking_id (checkouts) act as one when the header is absent. Rate limits, per key over a rolling hour: 60 writes and 600 reads by default (raised on request). Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; over the limit you get 429 with Retry-After. Routes: GET /v1/me your venue, limits and categories POST /v1/offerings create or update an offering (tiers, availability, policy inline) GET /v1/offerings your offerings GET /v1/offerings/{external_id} one offering POST /v1/checkouts create a checkout GET /v1/checkouts ?status=&limit=&before= GET /v1/checkouts/{id} POST /v1/checkouts/{id}/cancel releases every hold POST /v1/webhook_endpoints { url, events[] }; returns the signing secret (whsec_…) once GET /v1/webhook_endpoints DELETE /v1/webhook_endpoints/{id} POST /v1/experiences submit a dated Event for review GET /v1/experiences, /v1/experiences/{id} POST /v1/boats one boat, or { "boats": [...] } up to 100: a rental fleet's sync GET /v1/boats ?status=pending|approved|rejected|inactive&limit=&before= GET /v1/boats/{id} POST /v1/boats/{id}/deactivate the same as upserting the boat with "active": false GET /v1/analytics views, bookings, show rate POST /v1/integrations connect a venue system; returns its webhook URL once GET /v1/integrations, POST /v1/integrations/{id}/revoke POST /v1/hooks/{provider}/{token} where a connected system delivers (the URL's token is the credential) Everything you post is checked against the JSON Schemas before anything is stored. Offerings and events are reviewed by SplitClub before people see them. ## 6. Offerings: telling SplitClub what you sell One POST per offering, with everything inline: curl https://splitclubapp.com/api/v1/offerings \ -H "Authorization: Bearer sc_test_…" \ -H "Content-Type: application/json" \ -d @offering.json The answer is 201 the first time and 200 when the same external_id comes again (it's updated). To change one part later (new prices for the season, a new tee sheet), send the whole offering again with the same external_id, or use the MCP tools set_price_tiers and set_availability, which do that for you. If your booking system already has the data (a tee sheet, a class schedule, a menu), either post from your server on a schedule using the mapping file for that system (section 10), or connect the system's webhooks with POST /v1/integrations. ## 7. Pay with SplitClub: the checkout flow 1. A group books on your site or app. Your server creates the checkout, with the secret key: POST /v1/checkouts Idempotency-Key: { amount, currency, party_size, pay_by, description, return_url, external_booking_id, … } It answers 201 with the Checkout, including its url. 2. The guest opens it. On a web page, load the script and open the modal with your publishable key: SplitClub.button(element, options) renders the official "Pay with SplitClub" button. Or send the guest to checkout.url directly (email, SMS, an app). If the SplitClub app is installed, the link opens in it. 3. The organizer signs in with a phone code, chooses the party (SplitClub friends, a group chat, phone numbers, or a share link) and the split: equal, or custom amounts. In an equal split, leftover cents go to the organizer, and the shares always add up to exactly the checkout amount. Each person gets an invite link (https://checkout.splitclubapp.com/s/…) and pays their own share with a card or Apple Pay. A share is a card hold (an authorization), not a charge. 4. When every share is held the checkout is funded; SplitClub captures every hold, marks it completed, sends you checkout.completed, and schedules the venue's payout through Stripe Connect. 5. If pay_by passes first, every hold is released and the checkout expires (checkout.expired). Nobody is charged. POST /v1/checkouts/{id}/cancel does the same on demand. Confirm the booking in your system on the signed checkout.completed webhook, not on the browser callback: the browser can be closed, and onComplete can be faked. Test mode: with sc_test_/pk_test_ keys, pay with Stripe test cards (4242 4242 4242 4242, any future date, any CVC). Test checkouts never move money and are left out of analytics. ## 8. Webhooks and signature verification Register an endpoint once: curl https://splitclubapp.com/api/v1/webhook_endpoints \ -H "Authorization: Bearer sc_test_…" -H "Content-Type: application/json" \ -d '{ "url": "https://resort.example/hooks/splitclub", "events": ["checkout.completed", "checkout.expired"] }' The response shows the signing secret (whsec_…) once. Store it as a secret. Events: checkout.created, checkout.collecting, share.held, checkout.funded, checkout.completed, checkout.expired, checkout.canceled (and refund.created, offering.approved, offering.rejected). Every delivery is a POST with a WebhookEvent body: { id: "evt_…", object: "event", type, mode, created_at, data: { object: } } and a header: SplitClub-Signature: t=,v1= Verify it on the raw body bytes, before parsing, and reject a t more than 5 minutes from now. The HMAC key is the whole secret string, whsec_ included. Node: import crypto from "node:crypto"; export function verifySplitClub(rawBody, header, secret) { const p = Object.fromEntries(String(header).split(",").map((kv) => kv.split("=", 2))); const t = Number(p.t); if (!t || Math.abs(Date.now() / 1000 - t) > 300 || !p.v1) return false; const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); return want.length === p.v1.length && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(p.v1)); } Python: import hmac, hashlib, time def verify_splitclub(raw_body: bytes, header: str, secret: str) -> bool: p = dict(kv.split("=", 1) for kv in header.split(",")) t = int(p.get("t", "0")) if not t or abs(time.time() - t) > 300 or "v1" not in p: return False want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(want, p["v1"]) Answer 2xx quickly and do the work after. Failed deliveries are retried with exponential backoff for 72 hours, so the same event can arrive more than once: dedupe on its id. ## 9. Errors Every failure has one shape. error is the first problem, with a request_id to quote; errors lists every problem when a body has several: { "error": { "code": "invalid_type", "message": "…", "field": "/tiers/0/amount", "request_id": "…" }, "errors": [ { "code", "message", "field", "expected", "example_fix", "doc_url" }, … ] } field is a JSON pointer into what you sent ("" is the whole body). The messages are written so an AI agent can fix the body and retry without guessing. For example, this tier: { "label": "Weekend", "amount": 185.5, "currency": "USD", "unit": "per_player" } comes back as: [ { "code": "invalid_type", "message": "`/amount` must be an integer in minor units (e.g. 14500 for $145.00); you sent 185.5.", "field": "/amount", "expected": "an integer in minor units (e.g. 14500 for $145.00)", "example_fix": "\"amount\": 14500", "doc_url": "https://splitclubapp.com/developers#schema-price-tier" }, { "code": "invalid_format", "message": "`/currency` must be a three-letter ISO 4217 code in lower case (e.g. \"usd\"); you sent \"USD\".", "field": "/currency", "expected": "a three-letter ISO 4217 code in lower case (e.g. \"usd\")", "example_fix": "\"currency\": \"usd\"", "doc_url": "https://splitclubapp.com/developers#schema-price-tier" }, { "code": "invalid_value", "message": "`/unit` must be one of: \"per_person\", \"per_group\", \"per_hour\", \"per_unit\"; you sent \"per_player\".", "field": "/unit", "expected": "one of: \"per_person\", \"per_group\", \"per_hour\", \"per_unit\"", "example_fix": "\"unit\": \"per_person\"", "doc_url": "https://splitclubapp.com/developers#schema-price-tier" } ] HTTP statuses: 400 invalid_json the body isn't JSON 401 unauthorized the key is missing, wrong, revoked or expired 403 insufficient_scope the live key lacks the scope; also ip_not_allowed, wrong_key_kind, origin_not_allowed 404 not_found no such route, or not yours 405 method_not_allowed 413 payload_too_large over 64 KB 422 (validation codes) missing_field, unknown_field, invalid_type, invalid_value, invalid_format, out_of_range, too_long, too_short, too_many_items…; see errors[] 429 rate_limited wait Retry-After seconds 500 internal_error our side; retry with backoff and quote request_id ## 10. Provider mappings If the venue already runs a booking or point-of-sale system, its mapping file says how that system's objects and webhooks become SplitClub objects: which of their fields fills which of ours (as JSON pointers), how to convert money and times, which webhook events matter and how they're signed, and what can't be mapped. Every mapping cites the provider's public developer docs. A field marked "unverified": true wasn't confirmed in those docs (often because the provider keeps its API reference behind a partner login): confirm it with the venue or the provider before you rely on it. https://splitclubapp.com/schemas/v1/mappings/.json Format: https://splitclubapp.com/schemas/v1/mappings/_mapping.schema.json verified = every field confirmed in the provider's public docs; partial = some fields marked unverified; unverified = nothing could be confirmed (the docs aren't public). Golf Club Prophet (CPS): unverified, 0 of 2 fields confirmed; manual; webhooks not documented; API docs not public https://splitclubapp.com/schemas/v1/mappings/clubprophet.json foreUP: partial, 32 of 35 fields confirmed; poll and post; webhooks, not signed https://splitclubapp.com/schemas/v1/mappings/foreup.json GolfNow (Affiliate & Partner API 2.1): partial, 20 of 31 fields confirmed; poll and post; webhooks not documented https://splitclubapp.com/schemas/v1/mappings/golfnow.json Lightspeed Golf (Chronogolf): partial, 26 of 30 fields confirmed; poll and post; no webhooks https://splitclubapp.com/schemas/v1/mappings/chronogolf.json Spas and studios Boulevard: partial, 29 of 31 fields confirmed; post from the venue's server; signed webhooks (x-blvd-oauth-hmac-sha256) https://splitclubapp.com/schemas/v1/mappings/boulevard.json Mindbody: partial, 48 of 52 fields confirmed; post from the venue's server; signed webhooks (X-Mindbody-Signature) https://splitclubapp.com/schemas/v1/mappings/mindbody.json Zenoti: partial, 38 of 42 fields confirmed; poll and post; webhooks; signature scheme not documented https://splitclubapp.com/schemas/v1/mappings/zenoti.json Tickets and events DICE (Ticket Holders API for partners): partial, 16 of 30 fields confirmed; poll and post; no webhooks https://splitclubapp.com/schemas/v1/mappings/dice.json Eventbrite: partial, 25 of 34 fields confirmed; poll and post; webhooks, not signed https://splitclubapp.com/schemas/v1/mappings/eventbrite.json Tours, boats and rentals FareHarbor (External API v1): partial, 37 of 38 fields confirmed; poll and post; webhooks, not signed https://splitclubapp.com/schemas/v1/mappings/fareharbor.json Peek Pro (Peek Reseller API, OCTO): partial, 34 of 35 fields confirmed; poll and post; webhooks, not signed https://splitclubapp.com/schemas/v1/mappings/peek.json Restaurants and nightlife OpenTable: partial, 31 of 35 fields confirmed; webhook; webhooks, not signed https://splitclubapp.com/schemas/v1/mappings/opentable.json Resy (Resy OS): unverified, nothing mapped yet; webhook; webhooks not documented; API docs not public https://splitclubapp.com/schemas/v1/mappings/resy.json SevenRooms: unverified, nothing mapped yet; webhook; webhooks not documented; API docs not public https://splitclubapp.com/schemas/v1/mappings/sevenrooms.json Tock (Exploretock): partial, 21 of 26 fields confirmed; webhook; webhooks, not signed https://splitclubapp.com/schemas/v1/mappings/tock.json Point of sale Square: partial, 36 of 37 fields confirmed; webhook; signed webhooks (x-square-hmacsha256-signature) https://splitclubapp.com/schemas/v1/mappings/square.json Toast: partial, 30 of 30 fields confirmed; webhook; signed webhooks (Toast-Signature) https://splitclubapp.com/schemas/v1/mappings/toast.json Paths in a mapping file: webhook the provider pushes; connect it with POST /v1/integrations { provider } and paste the returned URL into the provider's webhook settings (providers SplitClub accepts there: sevenrooms, opentable, resy, tock, tripleseat, toast, square, clover, lightspeed, spoton, touchbistro, simphony, custom) poll_and_post read the provider's API on a schedule, map, and POST /v1/offerings post_from_partner_server the venue's own server already has the data: map it and POST directly ## 11. The MCP server Endpoint: https://splitclubapp.com/api/mcp Transport: MCP Streamable HTTP (protocol 2025-06-18; 2025-03-26 and 2025-11-25 clients work too). Stateless: every POST carries the key; each answer is a single JSON response. Auth: Authorization: Bearer sc_test_… (build with the test key; a console sandbox key works too). Test keys can use every tool; live keys need the tool's scope. A request that calls upsert_offerings, set_price_tiers, set_availability or create_checkout counts as a write against the key's hourly limit; everything else as a read. Tools (inputs are the v1 JSON Schemas, strict): describe_venue start here: the key's venue, mode, scopes, and the order of work. Read-only. validate { schema, object }: dry-run any object; returns valid, or every error with its JSON pointer. upsert_offerings { offerings: Offering[] } (up to 25): validated first; nothing is sent if any has an error. set_price_tiers { offering_external_id, tiers: PriceTier[] }: replaces that offering's tiers. set_availability { offering_external_id, availability }: replaces that offering's availability. create_checkout a CheckoutCreate; external_booking_id is the idempotency key. Returns the checkout and its url. get_checkout { id: "chk_…" } list_bookings { status?, limit?, before? }: the venue's checkouts, newest first. Resources: every schema (https://splitclubapp.com/schemas/v1/.json) and every provider mapping (https://splitclubapp.com/schemas/v1/mappings/.json), readable with resources/read. Prompt: connect_venue { venue, system? }: the walkthrough below, as instructions. Each tool calls the same Partner API route your server would, with your key: same validation, scopes, rate limits, idempotency and audit log. Connect a client: Claude Code: claude mcp add --transport http splitclub https://splitclubapp.com/api/mcp \ --header "Authorization: Bearer sc_test_…" Claude Desktop (claude_desktop_config.json, through the mcp-remote bridge): { "mcpServers": { "splitclub": { "command": "npx", "args": ["-y", "mcp-remote", "https://splitclubapp.com/api/mcp", "--header", "Authorization:${SPLITCLUB_AUTH}"], "env": { "SPLITCLUB_AUTH": "Bearer sc_test_…" } } } } Cursor (~/.cursor/mcp.json or .cursor/mcp.json): { "mcpServers": { "splitclub": { "url": "https://splitclubapp.com/api/mcp", "headers": { "Authorization": "Bearer sc_test_…" } } } } OpenAI (Responses API, remote MCP tool): { "type": "mcp", "server_label": "splitclub", "server_url": "https://splitclubapp.com/api/mcp", "headers": { "Authorization": "Bearer sc_test_…" }, "require_approval": "always" } ChatGPT's own connectors sign in with OAuth, which this server doesn't offer yet; use the API or another client. Raw protocol, to see it work: curl https://splitclubapp.com/api/mcp \ -H "Authorization: Bearer sc_test_…" -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"describe_venue","arguments":{}}}' ## 12. Walkthrough: hook up a golf resort, end to end The request: "Hey, can we hook up the golf resort to SplitClub for our payments?" Here is everything an agent (or an engineer) does, with the real JSON. The resort is Coral Ridge Golf Resort, its tee sheet runs on foreUP, and you have its test key. Step 1. Read the guide and confirm the key. Read https://splitclubapp.com/llms.txt, then this file. Call describe_venue (MCP) or GET /v1/me: it answers with the venue (Coral Ridge Golf Resort), mode "test" and the scopes. Step 2. Find out what the resort runs, and read its mapping. Ask which tee-sheet system it uses. For foreUP, read https://splitclubapp.com/schemas/v1/mappings/foreup.json. It says how a course and its tee sheet become an Offering with kind "tee_time", how its rates become PriceTiers (decimal dollars → integer cents), and how tee times become Availability slots, plus every field it couldn't verify. Anything unverified, confirm with the resort. Step 3. Write the offering: the course, three tiers, the daily tee sheet and the cancellation rule. Weekday $145 and weekend $185 a player, twilight $85 from 3pm; 2 to 4 players (1 to 4 at twilight); 7% sales tax added on top; tee times every 10 minutes from 7:00 to 16:50; closed Thanksgiving and Christmas; full refund 48 hours out, half at 24; the party must be paid 12 hours before tee time. { "external_id": "coral-ridge-champ-18", "kind": "tee_time", "title": "18 holes, Championship Course", "description": "Par 72, 7,100 yards. Cart and range balls included.", "media": [ { "url": "https://coralridge.example/img/18th-green.jpg", "alt": "The 18th green at dusk" } ], "booking_url": "https://coralridge.example/tee-times", "tiers": [ { "external_id": "weekday", "label": "Weekday", "amount": 14500, "currency": "usd", "unit": "per_person", "min_party": 2, "max_party": 4, "days": [ "mon", "tue", "wed", "thu", "fri" ], "fees": [ { "type": "tax", "label": "Sales tax", "percent": 7, "included": false } ] }, { "external_id": "weekend", "label": "Weekend", "amount": 18500, "currency": "usd", "unit": "per_person", "min_party": 2, "max_party": 4, "days": [ "sat", "sun" ], "fees": [ { "type": "tax", "label": "Sales tax", "percent": 7, "included": false } ] }, { "external_id": "twilight", "label": "Twilight", "amount": 8500, "currency": "usd", "unit": "per_person", "min_party": 1, "max_party": 4, "time_window": { "from": "15:00", "to": "19:00" }, "fees": [ { "type": "tax", "label": "Sales tax", "percent": 7, "included": false } ] } ], "availability": { "timezone": "America/New_York", "recurring": [ { "rrule": "FREQ=DAILY;BYHOUR=7,8,9,10,11,12,13,14,15,16;BYMINUTE=0,10,20,30,40,50", "starts_at": "2026-11-01T07:00:00-05:00", "until": "2027-03-31", "duration_minutes": 270, "capacity": 4 } ], "blackout_dates": [ "2026-11-26", "2026-12-25" ], "lead_time_minutes": 60, "cutoff_minutes": 720 }, "policy": { "cancellation": [ { "hours_before": 48, "refund_pct": 100 }, { "hours_before": 24, "refund_pct": 50 } ], "dress_code": "Collared shirts, soft spikes only.", "arrival_window_minutes": 30 } } Step 4. Validate it, fix everything, validate again. MCP: validate { "schema": "offering", "object": } → { "valid": true, "errors": [] }. If it isn't valid, every error says which field (/tiers/1/amount), what's expected and a fix. Fix them all at once, then validate again. Don't send until it's valid. Step 5. Send it (test mode). MCP: upsert_offerings { "offerings": [ ] } REST: POST /v1/offerings with the offering as the body. 201 Created; SplitClub reviews it before people see it. Step 6. Put Pay with SplitClub on the booking page. When a foursome books Saturday 7 November at 07:30 (weekend rate: 4 × $185.00 = $740.00, plus 7% tax = $791.80), the resort's server creates the checkout: POST https://splitclubapp.com/api/v1/checkouts Authorization: Bearer sc_test_… Idempotency-Key: foreup-res-88412 { "amount": 79180, "currency": "usd", "party_size": 4, "pay_by": "2026-11-06T19:30:00-05:00", "description": "Tee time for four, Sat 7 Nov, 07:30, Championship 18", "return_url": "https://coralridge.example/booking/confirmed", "cancel_url": "https://coralridge.example/tee-times", "external_booking_id": "foreup-res-88412", "offering_external_id": "coral-ridge-champ-18", "metadata": { "tee_time": "2026-11-07T07:30:00-05:00", "tier": "weekend" } } It answers 201: { "id": "chk_7Hc2Qm9xLp", "object": "checkout", "mode": "test", "status": "open", "amount": 79180, "currency": "usd", "party_size": 4, "pay_by": "2026-11-06T19:30:00-05:00", "description": "Tee time for four, Sat 7 Nov, 07:30, Championship 18", "venue": { "id": "9b2dc857-4c1e-4f7a-9d0e-2f6a1b3c4d5e", "name": "Coral Ridge Golf Resort" }, "external_booking_id": "foreup-res-88412", "url": "https://checkout.splitclubapp.com/c/chk_7Hc2Qm9xLp", "shares": [], "funded_amount": 0, "created_at": "2026-11-02T14:05:11-05:00", "metadata": { "tee_time": "2026-11-07T07:30:00-05:00", "tier": "weekend" } } The booking page opens it: The organizer picks the other three players and splits it equally: $197.95 each. Each pays their share as a hold. Step 7. Register the webhook and run one test checkout end to end. POST /v1/webhook_endpoints { "url": "https://coralridge.example/hooks/splitclub", "events": ["checkout.completed", "checkout.expired"] }, store the whsec_ secret, and pay all four shares with the test card 4242 4242 4242 4242. When the fourth hold lands, the resort receives, signed: { "id": "evt_4Rt8Wn2Kd", "object": "event", "type": "checkout.completed", "mode": "test", "created_at": "2026-11-03T09:12:40-05:00", "data": { "object": { "id": "chk_7Hc2Qm9xLp", "object": "checkout", "mode": "test", "status": "completed", "amount": 79180, "currency": "usd", "party_size": 4, "pay_by": "2026-11-06T19:30:00-05:00", "description": "Tee time for four, Sat 7 Nov, 07:30, Championship 18", "venue": { "id": "9b2dc857-4c1e-4f7a-9d0e-2f6a1b3c4d5e", "name": "Coral Ridge Golf Resort" }, "external_booking_id": "foreup-res-88412", "url": "https://checkout.splitclubapp.com/c/chk_7Hc2Qm9xLp", "shares": [ { "id": "shr_a1", "amount": 19795, "status": "captured", "is_organizer": true, "display_name": "Marcus R.", "held_at": "2026-11-02T14:09:02-05:00" }, { "id": "shr_a2", "amount": 19795, "status": "captured", "is_organizer": false, "display_name": "Dev P.", "held_at": "2026-11-02T15:30:47-05:00" }, { "id": "shr_a3", "amount": 19795, "status": "captured", "is_organizer": false, "display_name": "Luis M.", "held_at": "2026-11-02T18:02:19-05:00" }, { "id": "shr_a4", "amount": 19795, "status": "captured", "is_organizer": false, "display_name": "Chris B.", "held_at": "2026-11-03T09:12:31-05:00" } ], "funded_amount": 79180, "created_at": "2026-11-02T14:05:11-05:00", "completed_at": "2026-11-03T09:12:40-05:00", "metadata": { "tee_time": "2026-11-07T07:30:00-05:00", "tier": "weekend" } } } } Verify SplitClub-Signature (section 8), then mark foreup-res-88412 paid in foreUP. If the party isn't fully paid by pay_by, checkout.expired arrives instead, every hold is released, and the resort frees the tee time. Step 8. Go live. Hand the operator the switch: SplitClub issues sc_live_/pk_live_ keys once the resort is verified (domain or work email, Stripe Connect payouts set up). Swap the keys; nothing else changes. A prompt that does all of this, for any assistant connected to the MCP server: Connect Coral Ridge Golf Resort to SplitClub using my test key. It runs foreUP. Read https://splitclubapp.com/developers/llms-full.txt and the foreUP mapping, describe the venue, write one tee-time offering per course with weekday, weekend and twilight tiers, the tee sheet and the cancellation policy, validate each until it's valid, upsert them, then create a test checkout for a foursome next Saturday at 7:30 and give me its link. ## 13. The same pattern elsewhere Spa: Offering kind "spa_treatment" per treatment; tiers per duration (60 or 90 minutes) or per person; availability = appointment slots; policy = 24-hour cancellation. Nightclub: Offering kind "table" per section; tier unit "per_group" with minimum_spend and a deposit; availability = nights (slots); policy min_age 21 and a dress code. Beach club: "cabana" and "daybed" offerings, per_group tiers by weekday and weekend. Boat charter: Offering kind "boat_day"; tier per_group for 4 or 8 hours; availability = dates; policy = weather and cancellation rules. Shows: an Event per date with GA and VIP tiers and an on_sale window. In every case the checkout is the same call: the total, the party size and the deadline. ## 14. Security - The server sets the amount. A checkout's total comes only from your server call with the secret key; the modal, the page and the app can't change it. SplitClub computes the shares. - Secret keys never work in a browser (no CORS). Publishable keys can only open checkouts your server created, and only from your registered origins. return_url and cancel_url must be on a registered origin. - Every request is validated against the schemas: unknown fields refused, sizes capped. Every key action is audited. Keys can carry scopes, an expiry and an IP allowlist, and are revoked instantly. - Holds expire at pay_by; nothing is captured until the party is fully funded; refunds go back to the original card. - Webhooks are signed (section 8). Verify them. - Found a vulnerability? security@splitclubapp.com. ## 15. Versioning and contact This is version 1. Fields are added to responses without notice, so ignore the ones you don't use; anything that would break an integration ships as v2 at a new path. Schemas live at /schemas/v1/ for as long as v1 does. Partnerships and access: partners@splitclubapp.com Developers page: https://splitclubapp.com/developers Partner console (keys): https://splitclubapp.com/developers/console