# Mobile API Quickstart

This is the practical starting point for a mobile developer building the first Meets client.

## Base URLs

Use the HTTPS API proxy for staging:

```bash
API_BASE_URL=https://meets.cc/api/backend
WEB_BASE_URL=https://meets.cc
OPENAPI=https://meets.cc/api/openapi.yaml
HTTP_EXAMPLES=https://meets.cc/api/mobile-api-examples.http
```

Direct backend ports are bound to localhost on the server for security. Mobile apps must use the HTTPS API base URL.

The downloaded OpenAPI file is a deployed byte-identical mirror of the canonical backend contract. Repository and live mobile gates reject it if deployment falls behind the canonical source.

## First Screens To Build

| Screen | API calls | Auth |
| --- | --- | --- |
| Events list | `GET /events?limit=20` | Anonymous |
| Event detail | `GET /events/{event_id}` | Anonymous |
| Feed | `GET /feed?limit=20` | Anonymous, richer when signed in |
| Public profiles | `GET /users/profiles?limit=20` | Anonymous |
| My profile | `GET /me` | Required |
| Join event | `POST /events/{event_id}/join` | Required |
| My calendar | `GET /calendar/entries` | Required |
| Notifications | `GET /notifications` | Required |

Start with anonymous discovery first, then add login, join, calendar and notifications.

Public profile discovery is curated. `GET /users/profiles` excludes automated e2e/smoke/test profiles and may differ from a direct `GET /users/{user_id}/profile` lookup for an exact user URL.

## First Integration Checklist

1. Build anonymous discovery with `GET /events`, `GET /events/{event_id}`, `GET /feed` and `GET /users/profiles`.
2. Add phone auth (or dev auth in an enabled environment) and store `refresh_token` in secure storage; Yandex WebView remains a cookie-backed web flow.
3. Keep the returned `access_token` in memory and send `Authorization: Bearer <access_token>` on protected calls.
4. Implement `POST /auth/refresh` with JSON body and atomically replace both rotated tokens.
5. Add join event, calendar entries, notifications and 401 recovery.
6. Run the production-safe public/anonymous mobile smoke before handoff:

```bash
API_BASE_URL=https://meets.cc/api/backend WEB_BASE_URL=https://meets.cc API_CORS_ALLOWED_ORIGIN=https://meets.cc ./scripts/check-mobile-api-contract
```

Download runnable REST Client examples from `https://meets.cc/api/mobile-api-examples.http`.

## Auth Contract

Native mobile auth uses a short-lived opaque Bearer token:

- successful phone/dev auth and `POST /auth/refresh` return `access_token`, `access_token_expires_at`, `token_type: "Bearer"`, `refresh_token` and refresh expiry `expires_at`;
- send `Authorization: Bearer <access_token>` on protected API requests;
- access tokens expire after 15 minutes by default; refresh proactively or after a `401`;
- `POST /auth/refresh` accepts `{"refresh_token":"..."}` and rotates both tokens, immediately invalidating the previous pair;
- browser/WebView clients remain compatible through the `vstrechi_refresh_token` HttpOnly cookie (`Secure` over HTTPS, `SameSite=Lax`).

For a native MVP:

1. Prefer phone login through `POST /auth/phone/call/start`, show `call_phone_pretty`, then poll `POST /auth/phone/call/status` until `verified` or `expired`.
2. Persist `refresh_token` in platform secure storage; keep `access_token` in memory where possible.
3. Send `Authorization: Bearer <access_token>` on protected API requests.
4. Call `POST /auth/refresh` with the stored refresh token and replace both local tokens only after a successful response.

The current Yandex OAuth endpoint redirects back to the web app and intentionally exposes no token in the URL. Use phone auth for a fully native production token flow.

## Auth Errors And Session Rules

- `401` on a protected call may mean the access token expired. Attempt one synchronized refresh, retry the request once, then show the login gate if refresh also fails.
- `POST /auth/refresh` rotates access and refresh tokens; always replace both values from the response and prevent parallel refresh races.
- After `POST /auth/logout`, a repeated `GET /me` must return `401`; clear secure storage and the local profile.
- Browser/WebView clients must use an allowlisted origin. Native HTTP clients use the HTTPS API directly and are not limited by browser CORS.

Most API errors return JSON as `{"error":"code"}`. The mobile client should branch by HTTP status first, then use `error` for copy and diagnostics.

| Status | Error code | Meaning | Client action |
| --- | --- | --- | --- |
| `400` | `invalid_request` | Malformed JSON, invalid filter, missing field or invalid enum value. | Keep user on the current screen and show validation near the field/action. |
| `401` | `unauthorized` | Missing/expired session, invalid refresh token or logout completed. | Clear local session state and show login gate. |
| `403` | `forbidden` | User is authenticated but cannot perform this action. | Do not retry automatically; show a permission message and keep state unchanged. |
| `404` | `not_found` | Resource does not exist or is not visible to this user. | Show removed/not-found state and drop stale local references. |
| `409` | `username_taken` | Profile username conflict. | Keep profile form open and ask for another username. |
| `5xx` | `internal_error` | Unexpected server failure. | Show retry affordance and roll back optimistic state if needed. |

Dev/staging auth can use:

```bash
curl -sS -X POST "$API_BASE_URL/auth/dev/start" \
  -H 'Content-Type: application/json' \
  --data '{"email":"mobile@example.test"}'

curl -sS -X POST "$API_BASE_URL/auth/dev/verify" \
  -H 'Content-Type: application/json' \
  --data '{"email":"mobile@example.test","code":"123456"}'
```

Phone call auth uses SMS.RU Call Auth in production. `POST /auth/phone/call/start` returns the number the user should call; `POST /auth/phone/call/status` returns `pending`, `verified` with the normal auth response, or `expired`.

## Smoke Check

Run this from the repo before handing the API to mobile development:

```bash
API_BASE_URL=https://meets.cc/api/backend WEB_BASE_URL=https://meets.cc API_CORS_ALLOWED_ORIGIN=https://meets.cc ./scripts/check-mobile-api-contract
```

The default `public` mode checks HTTPS health, public reads, OpenAPI, anonymous auth gates, allowlisted browser preflight and blocked foreign origins without requiring or enabling dev auth.

Use `MOBILE_API_AUTH_MODE=token MOBILE_API_ACCESS_TOKEN=...` to additionally validate authenticated `/me`, calendar entries and notifications with an existing disposable access token. This mode does not rotate or revoke the supplied session.

Use `MOBILE_API_AUTH_MODE=dev` only against an environment where dev auth is explicitly enabled. It validates dev-auth cookie security, JSON-body refresh rotation, authenticated DTOs and logout cleanup. Production keeps dev auth disabled.

Across these modes the runner validates the mobile-critical JSON shape for events, feed, public profiles, auth responses, `/me`, calendar entries and notifications.

Successful write-path coverage runs only in the disposable Docker integration project:

```bash
./scripts/test-backend-repositories-integration
```

That gate starts isolated PostgreSQL, Redis and an API process with dev auth enabled; then two Bearer users exercise profile update (including `avatar_url`), event publish/join/leave, manual calendar CRUD, invite acceptance and notification read state. Cleanup cancels all active artifacts and revokes both sessions. The runner itself rejects non-loopback targets and requires `MOBILE_API_MUTATION_CONFIRM=local-disposable`, so it cannot be aimed at production accidentally.

## Mobile Integration Examples

Runnable `.http` collection is served from `https://meets.cc/api/mobile-api-examples.http`.

| Flow | Request | Expected | Client action |
| --- | --- | --- | --- |
| Anonymous discovery | `GET /events?limit=20`, `GET /feed?limit=20` | `200`, `items[]`, `next_cursor`; feed also returns `scope` | Render event cards, cache cursors, keep login optional until a mutating action. |
| Phone call auth and secure storage | `POST /auth/phone/call/start`, then `POST /auth/phone/call/status` | Start returns `call_phone_pretty`; verified status returns `current_user`, access/refresh tokens and expiries | Ask the user to call the returned number, poll status, then store refresh securely and use access as Bearer. |
| Dev auth and secure storage | `POST /auth/dev/start`, `POST /auth/dev/verify` | `200`, `current_user`, `session_id`, access/refresh tokens and expiries | Persist refresh securely and use access as Bearer; dev auth stays disabled in production. |
| Refresh then load profile | `POST /auth/refresh`, then `GET /me` | Refresh returns the rotated access/refresh pair; `/me` returns current user | Atomically replace both tokens, then fetch `/me` with the new Bearer token. |
| Join event | `POST /events/{event_id}/join` with `{"participation_visibility":"public"}` | `200` with participant status `going`, or `401` when anonymous | Update event detail, `participant_count`, `viewer_going` and calendar affordance after success. |
| Update profile | `PATCH /me/profile` with changed profile fields | `200` with the complete current-user DTO | Replace the cached profile only after success; `avatar_url` accepts HTTPS URLs or `null`. |
| Manual calendar entry | `POST /calendar/entries`, `PATCH /calendar/entries/{entry_id}`, `DELETE /calendar/entries/{entry_id}` | `201`, `200`, `200`; cancel returns status `canceled` | Reconcile the returned entry into local calendar state after every mutation. |
| Calendar invites | `GET /calendar/invites?role=incoming&status=pending&limit=20`, `GET /calendar/invites/{invite_id}`, then `PATCH /calendar/invites/{invite_id}` | Pending lists include `entry` and optional `inviter_profile`; detail also includes `participants`; patch accepts `accepted`, `declined` and `tentative` | Render pending invites in the calendar by `entry.starts_at`; use the detail endpoint for the full invite screen with organizer and participant statuses. |
| Calendar and notifications | `GET /calendar/entries`, `GET /notifications?limit=20`, `POST /notifications/{notification_id}/read` | `200` for signed-in user; `401` for anonymous user | Show login gate on `401`; render empty states when `items[]` is empty and persist returned `read_at`. |
| Logout cleanup | `POST /auth/logout`, then `GET /me` | `204` on logout; next `/me` returns `401` | Clear secure storage, cookie jar and local user/profile state. |

## Mobile-Critical DTO Fields

| DTO | Endpoints | Fields |
| --- | --- | --- |
| `EventCardDTO` | `GET /events`, `GET /feed` | `event.id`, `event.title`, `event.starts_at`, `event.image_url`, `event.status`, `location.city`, `participant_count`, `feed_context.viewer_going` |
| `CurrentUserDTO` | `GET /me` | `user.id`, `user.email`, `profile.user_id`, `profile.display_name`, `profile.avatar_url`, `profile.city`, `profile.interests` |
| `AuthSessionDTO` | `POST /auth/phone/call/status`, `POST /auth/dev/verify` | `current_user`, `session_id`, `access_token`, `access_token_expires_at`, `token_type`, `refresh_token`, `expires_at` |
| `RefreshSessionDTO` | `POST /auth/refresh` | rotated `access_token`, `access_token_expires_at`, `token_type`, `refresh_token`, `expires_at`; call `GET /me` with the new Bearer token |
| `CalendarEntryDTO` | `GET /calendar/entries` | `id`, `owner_user_id`, `source`, `event_id`, `title`, `starts_at`, `status`, `location_text` |
| `NotificationDTO` | `GET /notifications` | `items[]`, `unread_count`, `items[].kind`, `items[].title`, `items[].body`, `items[].href`, `items[].read_at` |
| `CalendarInviteDTO` | `GET /calendar/invites` | `invite.id`, `invite.status`, `invite.invited_by_user_id`, `entry.id`, `entry.title`, `entry.starts_at`, `entry.location_text`, `inviter_profile.user_id`, `inviter_profile.display_name`, `inviter_profile.username`, `inviter_profile.avatar_url` |
| `CalendarInviteDetailsDTO` | `GET /calendar/invites/{invite_id}` | top-level `id`, `status`, `entry_id`, `invited_by_user_id`, `entry.title`, `entry.starts_at`, `entry.location_text`, `inviter_profile.*`, `participants[].invite.status`, `participants[].profile.*` |

`inviter_profile` can be `null`; fall back to `invite.invited_by_user_id` and a generic inviter label when profile data is unavailable.

## Useful Curl Examples

```bash
curl -sS "$API_BASE_URL/healthz"
curl -sS "$API_BASE_URL/events?limit=20"
curl -sS "$API_BASE_URL/feed?limit=20"
curl -sS "$API_BASE_URL/users/profiles?limit=20"
curl -sS "$WEB_BASE_URL/api/openapi.yaml"
```

Refresh with a native-stored token:

```bash
curl -sS -X POST "$API_BASE_URL/auth/refresh" \
  -H 'Content-Type: application/json' \
  --data '{"refresh_token":"REFRESH_TOKEN"}'
```

Authenticated request:

```bash
curl -sS "$API_BASE_URL/me" \
  -H 'Authorization: Bearer ACCESS_TOKEN'
```

Join an event:

```bash
curl -sS -X POST "$API_BASE_URL/events/EVENT_ID/join" \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  --data '{"participation_visibility":"public"}'
```

## Known Beta Limits

- Browser CORS is allowlist-based through `API_CORS_ALLOWED_ORIGINS`; staging allows `https://meets.cc`. Native HTTP clients are not limited by browser CORS.
- Payments are disabled in beta: `PAYMENTS_ENABLED=false`.
- Incognito attendance is hidden from public profile/social feed, but visible to the current user in their own going/calendar surfaces.
- The staging dataset may change during QA; do not hardcode event IDs.
- Direct backend ports are not public; use `https://meets.cc/api/backend` from mobile clients.
