# BayMove Mobile API v1: shared contract (Laravel API, iOS app and Android app all build against this)

Base URL: {APP_URL}/api/v1. Requests and responses are JSON. The client always sends `Accept: application/json`.
Auth: Laravel Sanctum personal access tokens, sent as `Authorization: Bearer <token>`. A token is issued per device (`device_name`).
Errors: 401 {message}; 422 Laravel validation {message, errors: {field: [msg]}}; 429 when throttled.

## Auth
POST /auth/register   {name, email, password, password_confirmation, device_name, terms?}  -> 201 {token, user}
  When Jetstream terms are enabled (as on BayMove), `terms` is **required** and must be accepted (`terms: true`). Missing or rejected terms returns 422 `errors.terms` — the same explicit consent as the web registration form. Link users to the legal pages below before they register.
POST /auth/login      {email, password, device_name}                               -> 200 {token, user} | 422 (wrong credentials; generic message)
  When the user has confirmed two-factor authentication: 422
  `{message, two_factor_required: true, challenge_token, expires_in: 300, errors: {}}` — no token. Complete sign-in with POST /auth/two-factor.
POST /auth/two-factor {challenge_token, code? | recovery_code?, device_name}         -> 200 {token, user} | 422 errors.code | 429
  On 422: `challenge_invalid` is `true` when the challenge is expired, already used, invalidated after 5 failures, or unknown; `false` when the challenge is still live but the code or recovery code is wrong.
  Challenge is bound to user + device_name, single-use, 5 minute TTL, invalidated after 5 failed attempts. Throttled per challenge and per IP. Recovery codes are burned via Fortify. Never send email + code.
POST /auth/logout     (auth)                                                       -> 204 (revokes the current token only)
GET  /me              (auth)                                                       -> 200 {user}
POST /auth/forgot-password {email}                                                 -> 202 {message} (always 202; reuses the site's existing password reset email)
User = {id, name, email, email_verified_at|null, created_at, two_factor_enabled, has_password}
  `has_password` is false for social-login-only accounts (stored password is null). Emails are trimmed and lowercased on register, login, forgot-password (Fortify-compatible).
Register and login are throttled. Register uses the same validation, password rules, explicit terms acceptance, and side effects (events) as web registration.

### Legal pages (link from the apps before register)

Release builds should link to `config('baymove.web_base_url')` plus `config('baymove.legal_urls.terms')` and `privacy` (defaults: https://baymove.co.uk/terms-of-service and …/privacy-policy). Named routes `terms.show` and `policy.show` match those paths.

## Properties (public, no auth required)
GET /properties
  query: q (free text: address, town, postcode, area), area (area slug), listing_type (sale|rent),
         property_type, min_price, max_price (whole pounds), min_bedrooms, sort (newest|price_asc|price_desc),
         page, per_page (default 20, max 50)
  -> 200 {data: [PropertySummary], meta: {current_page, last_page, per_page, total}, links: {next|null, prev|null}}
  Only shows what the website's public search shows (live/published listings). Uses the same search backend (Scout/Meilisearch with the existing fallback).
  Default sort when omitted: recommended (premium boost, then recency). `sort=newest`: `published_at` desc, then `id` desc.
GET /properties/{slug}  -> 200 {data: PropertyDetail} | 404
  `address_line`, `postcode`, and `agent` match the public listing page (search cards expose town + postcode only, so search `address_line` is null; detail uses line1 + town like the page heading).
GET /areas              -> 200 {data: [{slug, name}]} (for the area filter)

PropertySummary = {id, slug, title, address_line, town, postcode, area: {slug, name}|null,
  listing_type: "sale"|"rent", property_type|null, price: int (whole pounds), price_label: string (e.g. "£250,000" or "£950 pcm"),
  price_qualifier|null, bedrooms|null, bathrooms|null, thumbnail_url|null, status_label|null, published_at, web_url}
PropertyDetail = PropertySummary + {description (plain text), features: [string], images: [{url, caption|null}],
  latitude|null, longitude|null, epc_rating|null, tenure|null, council_tax_band|null, agent: {name, phone|null, email|null, logo_url|null}|null}
Any field the data model doesn't have is returned as null, never left out. Image URLs are absolute.

## Brand (match the website)
Colours: navy #0B1F3A (primary and dark backgrounds), teal #0D9B8A (accent and primary buttons), teal-dark #0A7F71 (pressed state),
slate #4A5D73 (secondary text), mist #E8EEF2 (light surfaces). Font: Plus Jakarta Sans (Google Fonts, OFL), with the system font as fallback.
Icons and logo: m1kfb/baymove `public/` (apple-icon-180x180.png, ms-icon-310x310.png, android-icon-192x192.png) and the site header logo component in resources/js.

## Contract addendum (6 Oct): store and QA requirements
DELETE /account (auth) -> 204 | 422 (never 401 when the bearer token is valid)
  Users with a stored password must send `{password}`. Wrong or missing password returns 422 `{message, errors: {password: [...]}}`; sending `{confirm:"DELETE"}` without a password is treated as missing password (422 `errors.password`), not account deletion.
  Users whose stored password column is null (social-login-only) must send `{confirm:"DELETE"}` instead; wrong or missing confirm returns 422 `errors.confirm`. The server chooses which mode applies from the authenticated user's stored password, never from client-sent flags.
  Revokes ALL of the user's tokens, then deletes user data including saved searches, enquiries and favourites. Reuses Jetstream's DeleteUser action where possible.
Public web page on the site, `/account/delete` (named route `account.delete.show`), explaining how to delete your account in the app or on the
  web (logged-in users get a delete form there). Google Play needs this page.
POST /auth/forgot-password always returns the same 202 and message, whether or not the email exists.
Throttling: login, register, forgot-password, two-factor, and DELETE /account are all rate limited (429 JSON).
The /api/v1 group always returns JSON errors (404, 401, 405, 429, 500), even for unknown paths. Never redirect to a login page.
Privacy: `agent` is only ever a business estate agent's public contact details. Never expose private seller or landlord
  personal data, internal notes, or draft/unpublished/withdrawn listings.
Apps: Release builds use HTTPS only. Cleartext is allowed only in debug, for local Sail. Tokens live in Keystore-backed
  storage (Android) or the Keychain (iOS). The account screen has "Delete account": confirm, enter password, call
  DELETE /account, then sign out locally. Empty search results show a friendly empty state, including when there are
  zero live listings in total.

## Implementation notes
- `slug` on listings is the numeric listing id as a string (the website route key). There is no separate slug column yet.
- `agent.email` is always `null` in API responses today: the public property page shows agency name, logo, and phone only (no public agency inbox field on `Team`). Never expose a Jetstream user's login email.
