# HAYAA Sports Hub API and iOS Integration

Base URL: `https://hayaaeagles.com/api/v1`

Version: 1.0.0. Enabled sports: `basketball`, `baseball` (includes softball). Football is not part of this release. All IDs are scoped: use `(sport, id)` for sport records. Hub account IDs and sport member IDs are different identifiers.

## Architecture

The hub owns login, verified email, password recovery, and verified links to sport accounts. Sport databases remain authoritative for registrations, children, coach roles, teams, schedules, announcements, and conversation history. The hub forwards to fixed, allowlisted HTTPS sport services. It issues a one-minute internal token for an authenticated linked member and revokes that token after the request. No database credentials, SMTP credentials, or internal tokens belong in the iOS app.

Hub identity tables are isolated `hub_*` tables in the basketball database. This is logical separation, not a new database server. Each sport has identical `community_*` messaging tables. A hub account can link one member account per sport; each member may have multiple verified children. A sport member can belong to only one hub account. Existing accounts are not merged by matching names or email addresses.

## Response and Transport

Use HTTPS and JSON. Most endpoints return:

```json
{"success":true,"version":"1.0.0","data":{},"meta":{}}
```

`meta` is optional. Errors use the same envelope with `success:false` and `data.message`. Handle 401 (sign in), 403 (verification/permissions), 404, 405, 409 (conflict), 422 (validation), 429 (back off), and 5xx (retry with backoff). IDs and numeric database values may arrive as JSON strings; decode them flexibly. Dates from sport databases use the site's Eastern local time; do not assume UTC. Calendar dates without a time are all-day dates. Treat text as text, and sanitize any public-content HTML before rendering it in a web view.

Legacy registration/payment/contact relays can return their existing JSON shape instead of the envelope. In particular, inspect `error`, `success`, `clientSecret`, and `status` where applicable. Fetch the current registration schema before building the registration form. Never compute the payable total on-device.

Cookie-authenticated POSTs, including initial login, require `X-CSRF-Token`. First call `GET /session`, retain the returned cookie in URLSession's cookie storage, and send its `csrf_token`. Following login, store the returned bearer token in Keychain and send `Authorization: Bearer TOKEN`; authenticated bearer POSTs do not require CSRF. Use an ephemeral cookie store for separate users or clear it at logout. Never log tokens, passwords, message bodies, or child information. The hub does not implement OAuth; do not use historical agent OAuth metadata for app login.

## Hub Account Endpoints

| Method | Path | Input / Result |
| --- | --- | --- |
| GET | `/health` | Service status and supported sports. |
| GET | `/session` | CSRF token and optional signed-in hub account. |
| POST | `/auth/register` | `email`, `password`, `first_name`, `last_name`, `terms:true`. Sends email verification; no account details are disclosed for an existing address. |
| POST | `/auth/login` | `email`, `password`. Returns `token`, `expires_in:2592000`, and `account`. Unverified users can sign in but cannot link sports or message. |
| POST | `/auth/verify` | `token` from email. Single-use, one-hour expiry. Also confirms pending email changes. |
| POST | `/auth/resend-verification` | Signed-in user; empty object. Throttled. |
| POST | `/auth/forgot-password` | `email`. Generic response, email reset link if applicable. |
| POST | `/auth/reset-password` | Email `token`, new `password`. Revokes bearer tokens and invalidates prior hub sessions. |
| POST | `/auth/logout` | Empty object. Revokes all hub bearer tokens and associated browser sessions for the account. |
| GET | `/account` | Hub account and linked `sports:[{sport,member_id}]`. |
| POST | `/account/profile` | `first_name`, `last_name`. |
| POST | `/account/password` | `current_password`, `password`. Sign in again afterward. |
| POST | `/account/email` | `current_password`, `email`. Old email remains in use until the new address is verified. Does not change sport-site contact emails. |
| POST | `/account/link-sport` | `sport`, sport-account `email`, sport-account `password`. Sport account must be active, email-verified, and have changed its temporary password. Existing links conflict rather than being silently replaced. |
| POST | `/account/create-sport-account` | `sport`. Creates and links a member only when no account already exists for the verified hub email. For separate sport-site login, set a sport password using that site's reset flow. |

Passwords require at least ten characters, upper/lowercase letters, and a number. Account fields include `id`, `email`, `first_name`, `last_name`, `verified_at`, `disabled`, and `created_at`. Check verification before presenting messaging actions.

Example login flow:

```text
GET /session -> retain cookie + csrf_token
POST /auth/login {email,password}, X-CSRF-Token: csrf_token
Store response.data.token in Keychain
GET /account, Authorization: Bearer token
POST /account/link-sport {sport:"baseball",email,password}, Authorization: Bearer token
GET /sports/baseball/community/me, Authorization: Bearer token
```

Email links open `https://hayaaeagles.com/account.php?purpose=verify|reset&token=...`. The web page completes the flow. The app can also consume the token through the corresponding endpoint when implementing universal links; universal-link association files and app signing configuration are not included in this backend release.

## Public Sport API Parity

Prefix all paths below with `/sports/{sport}`. Both sports implement the same endpoints, using their own content and registration configuration.

| Method | Paths | Notes |
| --- | --- | --- |
| GET | `/health`, `/bootstrap`, `/site`, `/home` | Branding, theme, site settings, home content, upcoming games. |
| GET | `/about`, `/programs`, `/programs/in-house`, `/programs/travel` | Public program pages only. |
| GET | `/faqs`, `/news`, `/news/{slug}`, `/gallery`, `/videos` | News supports `page`, `limit` (1-100). |
| GET | `/documents`, `/meeting-minutes`, `/resources` | Download links and resource groups. |
| GET | `/board-members`, `/information-pages`, `/info/{slug}`, `/seo-pages` | Site content and navigation. |
| GET | `/divisions`, `/divisions/full`, `/leagues`, `/venues`, `/teams` | Teams accepts `division_id`. Hidden divisions are filtered where supported by the existing public API. |
| GET | `/teams/{id}/roster`, `/players/{id}/stats`, `/game-results` | Existing public roster/stats contract; no family contact information. Results accepts `division_id`. |
| GET | `/schedules/games`, `/schedules/games/{id}` | List filters: `division_id`, `team_id`, `gender`, `age_group`, `from`, `to`, `page`, `limit`. Detail includes calendar download link. |
| GET | `/schedules/practices`, `/schedules/practices/{id}` | List filters: `division_id`, `team_id`, `gender`, `age_group`, `page`, `limit`. |
| GET | `/events/special`, `/tournaments`, `/events/tournaments`, `/tournaments/{id}` | Special events and brackets. |
| GET | `/registration/status`, `/registration/schema`, `/registration/periods` | Current open leagues, allowed divisions, fees, options, volunteer rules, and field schema. |
| POST | `/registration/quote`, `/registration/total` | Existing registration fields. Server-calculated price, discounts, and volunteer opt-out requirements. |
| POST | `/registration/payment-intent` | Existing registration fields; return value consumed by Stripe's iOS SDK. |
| POST | `/registration/finalize` | Validated fields and `stripe_payment_intent` when payment is required. Never retry with a different payment intent for the same paid registration. |
| GET | `/sponsors`, `/sponsors/packages` | Active sponsor content/packages. |
| POST | `/sponsors/payment-intent` | Multipart form: `sponsor_name`, `sponsor_url`, `package_id`, `contact_name`, `phone`, `email`, `expiration_date`, `logo_file` (PNG/JPEG/WebP, max 5 MB). |
| POST | `/contact` | `first_name`, `last_name`, `email`, `phone`, `message`. |
| POST | `/board-members/email` | `member_id`, `sender_name` (max 120), `sender_email`, `subject` (min 3), `message` (min 10); omit honeypot `website`. |
| POST | `/newsletter` | `email`. |
| POST | `/newsletter/unsubscribe` | `email`. |
| GET | `/messages/success` | Registration/sponsorship completion copy. |
| POST | `/push/register`, `/push/subscriptions`, `/push/unregister` | Legacy device subscriptions; see below. |

`GET /calendar` retains the existing hub calendar API. Its current sources include the existing football calendar; football accounts and messaging are not enabled by this release.

Registration fields include league/division, player name, grade, height, years played, gender, birth date, jersey size and three jersey choices, parent name, email, phone, address, city, state, ZIP, sponsor interest, optional discount/volunteer/photo/coaching choices, and mandatory concussion/medical agreement flags. The schema is authoritative for options and requirements; do not hardcode basketball choices into a baseball UI. Agreements currently use the existing website documents. Account creation/claiming is separate from registration completion; use a verified family claim to link older or newly registered children.

Legacy push payloads use `installation_id`, iOS hex `device_token`, `platform`, optional `app_version`, `os_version`, `device_model`, and `subscriptions` keyed by `registration`, `schedule`, `events`, `news`. Messaging currently uses polling, not push notifications. Baseball APNs delivery requires its own configured app credentials; creating a device subscription does not establish APNs delivery. Do not treat this legacy endpoint as an authenticated member notification API.

## Family and Messaging API

All routes below require a verified hub account linked to the specified sport, plus an active sport account with verified email and changed temporary password. Prefix: `/sports/{sport}/community`.

| Method | Path | Input / Result |
| --- | --- | --- |
| GET | `/me` | Sport account, verified children, messaging status, current scopes, assigned publish scopes, and family-account URL. |
| GET/POST | `/profile` | Read profile, or update `first_name`, `last_name`, optional `phone`. |
| GET | `/claims` | Own recent child claims and review status. |
| POST | `/claims` | `first_name`, `last_name`, `birth_date` (YYYY-MM-DD). Creates pending guardian claim for sport-admin review; never grants access from a name match. |
| POST | `/child-consent` | `account_id` (child sport login), `enabled:true|false`. Verified guardians only. |
| GET | `/recipients?after=0` | `{items:[{id,name,is_coach,is_staff}],next_after,has_more}`. Continue while has_more even when a filtered page has no items. No global child directory. |
| GET | `/conversations` | `{items,next_before,monitoring}`. Up to 50, newest conversation IDs first. Use `before` for older pages. |
| GET | `/conversations?child_account_id=N` | Guardian read view for a linked child's conversations. ID is a sport member ID, not player or hub ID. |
| POST | `/conversations` | `recipient_id`. Returns conversation `id`; repeated requests return the existing pair. |
| GET | `/conversations/{id}/messages?after=0` | `{items,next_after,has_more}`, up to 100 in ascending message-ID order. Repeat immediately while has_more, then poll. |
| POST | `/conversations/{id}/messages` | `body` (1-4000 characters), `client_id` (16-64 letters/digits/underscore/hyphen). A UUID works. Reuse client_id for retries of the same send; returns original ID. |
| POST | `/conversations/{id}/read` | `message_id`. Monotonic read cursor; must reference a message in that conversation. |
| GET | `/announcements` | `{items,next_before}`. Up to 50 newest; `before` loads older items. Current scope membership controls visibility. |
| POST | `/announcements` | Assigned coach/staff only: `scope_type:"team"|"division"`, `scope_id`, `title` (1-160), `body` (1-4000). |
| POST | `/reports` | `message_id`, `reason` (1-1000). Reporter must have conversation access. Appears in sport admin review queue. |

Message item fields: `id`, `sender_id`, `body`, `created_at`, `first_name`, `last_name`. Conversation items contain `account_a`, `account_b`, `a_first`, `a_last`, `b_first`, `b_last`, `unread`. Unread counts exclude messages authored by the viewing account. Guardian read markers are separate from the child's markers. Conversation list ordering is by conversation creation ID, not most recent message.

Announcements contain author ID/name, scope type/ID, title/body, and created time. There are no attachments, message edits/deletions, typing indicators, or background message push in this release. Reports preserve the original message for review. The author cannot erase history that guardians can monitor.

### Access Rules

- A guardian needs a current `verified` `parent_guardian` link to the registered player. Coach/other relationships do not grant parental oversight.
- Sport administrators approve a separate player login tied to a registered player and verified guardian. The guardian must then enable messaging. A player account cannot be converted from an account already carrying family links.
- Guardians can read the child's full conversation history, including older messages, and disable messaging. They cannot send as the child.
- Direct participants retain history access while otherwise eligible to sign in. Sending requires current shared team/division scope and enabled player consent. A removed coach assignment blocks future sending; unrelated users cannot read conversation IDs.
- Coach/staff roles and scopes are independent. A coach flag alone does not grant division/team messaging privileges. Roles and assignments are re-evaluated, not embedded permanently in native tokens.
- A team assignment does not grant other teams' scope. Parent/player division membership also permits contacts in that division. There is no cross-sport direct messaging in this release.
- New messages are limited to 30 per minute per account; announcements to ten per hour. Authentication, claims, linking, and mail actions are also throttled.

## Admin and Website

Basketball: `https://hayaabasketball.com/admin/community.php`

Baseball: `https://hayaabaseball.com/admin/community.php`

Sidebar: Management > Team Messaging. Initially available to the existing super-admin role; grant other admin roles through existing page permissions as appropriate.

1. In Family Accounts, mark verified adults as coach/staff.
2. In Team Messaging, assign one or more teams/divisions.
3. Approve family claims in Family Accounts.
4. Approve a separate player login in Team Messaging using the registered player ID and verified guardian.
5. Guardian enables messaging in the website Messages screen or app.
6. Review reports in Team Messaging; disable a member through Family Accounts if needed.

Hub website: `/account.php`, `/messages.php?sport=basketball`, `/messages.php?sport=baseball`. Existing sport logins also use `/messages.php`. Logging into a sport does not automatically log into the hub. The hub offers linking to keep the identities connected without sharing browser cookies across domains.

## iOS Implementation Notes

Use URLSession and Keychain. Keep a selected sport, but key local caches by hub account + sport. Clear all private caches on logout/account switch. Use the hub token only with the hub host. Show verification/linking requirements as actionable account states.

Poll visible messaging screens every 15 seconds. Pause while the app is backgrounded; retry failures with exponential backoff capped at two minutes. Fetch additional message pages immediately when has_more is true. Do not advance the cursor until messages have been persisted/rendered. Send one stable UUID per message and preserve it for a network retry. Disable duplicate taps while a send is pending. Refresh announcements and unread counts on foreground entry.

Guardian monitoring must be clearly marked read-only, with the child's name visible. Never reuse the normal send composer while viewing a child's history. Expose enable/disable controls only for verified guardians. Show assigned announcement scopes from `publish_scopes`; do not infer authority from the coach flag alone.

Keep light mode, centered header logo, right hamburger, left back button where needed, and the floating bottom navigation requested for the app. Use each sport's `/bootstrap` branding and the hub logo for shared account views. No App Store release, universal-link entitlement, or iOS binary is included here.

## Verification and Limits

Both sports passed 29 public GET checks each. Transaction-scoped access tests covered child consent, assigned-coach contact, unrelated-user denial, guardian reads, guardian impersonation denial, revoked assignments, and revoked guardian access. Live HTTP fixture tests covered hub-to-sport authentication, cross-sport denial, send/retry, reporting, announcements, and token revocation; fixture records were removed afterward.

No real player registration, charge, refund, or customer email was generated for testing. Email inbox delivery and APNs delivery require a recipient/device acceptance test. SMTP uses the existing Google settings through PHPMailer. Never send credentials in this handoff or embed them in an app.
