# iShapps HCP Mobile API — Reference (SaaS backend)

This is the API the HCP (worker) mobile app talks to on the new multi-tenant iShapps backend.

**It keeps the v2 contract.** The endpoint names, request fields, response envelope and message strings are the same as the v2 API the app was built against. Where behaviour had to change, the entry is marked **⚠️ Changed**. Please check those against the app first. [§11](#11-checklist-for-the-app-team) lists them all in one place.

---

## 1. Base URLs

| | URL |
|---|---|
| Base URL | `{BASE_URL}` — ask the backend team for each environment. Local dev: `http://127.0.0.1:8000` |
| Org lookup (central, no tenant) | `{BASE_URL}/api/v2/tenancy/resolve` |
| Every app endpoint (tenant) | `{BASE_URL}/api/v2/<endpoint>` |

⚠️ **Changed:** the prefix is now `/api/v2/`. For example, v2's `/api/login` is now `/api/v2/login`, and `/api/shift/accept` is now `/api/v2/shift/accept`.

- Every endpoint is `POST`, as in v2.
- Send `Accept: application/json`.
- Bodies may be JSON or `multipart/form-data` (multipart is required for uploads).

---

## 2. Organisation (tenant) selection — ⚠️ NEW, required

The backend now serves many agencies (organisations). **Every app request must carry the organisation's id** in the `X-Tenant-Code` header. v2 never sent this, so **the app needs an update for this step.**

### Flow: email first, then password

1. The user types their email.
2. The app calls `POST {BASE_URL}/api/v2/tenancy/resolve` with `{ "email": "..." }`. This call needs no auth and no tenant header, and is limited to 6 requests per minute.

   ```json
   200
   {
     "tenants": [
       { "tenantId": "5369b519-d759-4fce-bba4-089a941b64d8", "name": "Smart Healthcare Solutions", "logoUrl": "https://…" }
     ]
   }
   ```

   - An empty list means no account exists for that email.
   - More than one entry means the user belongs to several organisations. Let them pick one.
   - A single entry can be selected automatically.
3. Store the chosen `tenantId`. Send it on **every** request from then on, login included:

   ```
   X-Tenant-Code: 5369b519-d759-4fce-bba4-089a941b64d8
   ```

4. Call `POST /api/v2/login` with the email and password.

The access token only works for the organisation it was issued in. If the user switches organisation, they must log in again under the new `X-Tenant-Code`.

---

## 3. Authentication

- `login` returns an `access_token`. Send it as `Authorization: Bearer <access_token>` on every endpoint except the public ones marked below.
- Only **HCP accounts** can log in to the app. An admin or facility account gets the normal "login failed" answer.
- A missing, expired or invalid token returns:
  ```json
  401 { "status": false, "message": "Unauthenticated." }
  ```

---

## 4. Response envelope (same as v2)

```json
{ "status": true,  "message": "Hcp found.", "data": { ... } }   // success, HTTP 200
{ "status": false, "message": "Shift not available.", "data": null } // business failure, HTTP 200
{ "status": false, "message": "Oops! Something went wrong.", "data": null } // server error, HTTP 500
```

- **Branch on `status`, not the HTTP code.** Business failures (not found, not allowed, already done) are HTTP **200** with `status: false`.
- A successful write with no payload returns `data: []`, an empty JSON **array**, as in v2.
- Lists use Laravel's paginator object. There are 200 items per page; send `page` for the next page.

  ```json
  { "current_page": 1, "data": [ ... ], "first_page_url": "…?page=1", "from": 1, "last_page": 1,
    "last_page_url": "…?page=1", "links": [ ... ], "next_page_url": null, "path": "…",
    "per_page": 200, "prev_page_url": null, "to": 3, "total": 3 }
  ```
  (`links` is a new, extra key; ignore it.)

### Validation errors (HTTP 422)

```json
{ "status": false, "message": "Validation failed.", "errors": { "shift_id": ["The shift id field is required."] } }
```

`errors` is an object keyed by field name, each holding an array of messages. v2 sent exactly this shape too.

### Throttling
- `login` and `forgot-password`: 10 requests per minute. Over the limit returns HTTP 429.

---

## 5. Public endpoints (tenant header, no token)

### `POST /login`
| Field | Rules |
|---|---|
| `email` | required, email |
| `password` | required |
| `device_id` | optional — the push (OneSignal) player id |
| `platform`, `device-type`, `device-name`, `device-version` | optional |

**Success:**

```json
{
  "status": true, "message": "Login successfully.",
  "data": {
    "access_token": "eyJ0…",
    "user": {
      "id": 12, "name": "…", "email": "…", "role_name": "hcp", "device_id": "…", "require_password_reset": 0, "...": "…",
      "hcp": {
        "id": 34, "hcp_prefix": "#HCP", "first_name": "…", "image": "file.jpg", "cv": "cv.pdf",
        "tfn_number": "…", "bank_name": "…", "app_registration_screen": "0", "...": "…",
        "image_url": "https://…signed…", "cv_url": "https://…signed…",
        "average_rating": 4.5,
        "hcp_professions": [ { "id": 1, "profession_id": 7, "level_id": 3, "status": "active", "profession": {…}, "category": {…}, "level": {…} } ]
      }
    }
  }
}
```

- ⚠️ **New fields:** `hcp.image_url` and `hcp.cv_url`. See [§10 Files](#10-files--changed).
- `require_password_reset: 1` means the user signed in with a temporary password (from forgot-password or an admin reset). Force the change-password screen.

**Failures:**
| When | Response |
|---|---|
| Wrong email or password, or not an HCP account | **422** `{"status":false,"message":"Either e-mail address or password is incorrect.","data":null,"errors":{"email":["Either e-mail address or password is incorrect."]}}` |
| Account inactive or archived | 200 `status:false`, `"Your account is inactive. Please contact administrator."` |

⚠️ An unknown email now gets the same "incorrect" answer as a wrong password. v2 told you the email didn't exist.

### `POST /forgot-password`
- Body: `email` (required).
- Response: always 200 `status:true` with `"We have emailed your new password."`, `data: null`, whether or not the email exists.
- The email contains a temporary password. The next login returns `require_password_reset: 1`.

### Reference data

| Endpoint | Body | `data` on success | Empty → message |
|---|---|---|---|
| `get-countries` | — | `{countries: [...]}` | `Country not available.` |
| `get-states` | `country_id?` | `{states: [ {…, cities: [ {…, suburbs: [...]} ]} ]}` | `State not available.` |
| `get-cities` | `state_id?` | `{cities: [...]}` | `City not available.` |
| `get-suburbs` | `city_id?` | `{suburbs: [...]}` | `Suburb not available.` |
| `get-categories` | — | `{categories: [...]}` (active only) | `Category not available.` |
| `get-levels` | `profession_id` (**needed**), `category_id?` | `{levels: [...]}` (active, in level order) | `Level not available.` |

- Success messages read "Country found.", "State found." and so on.
- ⚠️ `get-levels` without `profession_id` returns "not available". v2 only worked when both ids were sent.
- ⚠️ Professions and levels no longer include `mark_up`, `facility_per_rate` or `admin_fees`. Those are facility-side prices.

### `POST /get-common-content`
- Body: `per_page?`, `page?` (these apply to `documents`).
- Success message: `Common Content found.`

```json
"data": {
  "documents": { paginator of HCP general document types },
  "image_path": { "hcp_path": "", "supporting_page_path": "", "invoice_path": "", "facility_path": "" },
  "configuration": {
    "tracking_hcp": …, "tracking_before_start_job_in_minutes": …, "tracking_every_minutes": …,
    "shift_start_before_mint": …, "hcp_cust_care_email": "…", "android_app_link": "…", "ios_app_link": "…",
    "android_app_version": "…", "ios_app_version": "…", "show_accept_lower_level_job_checkbox": 0 (always: retired),
    "show_shift_accept_popup": true, "shift_accept_popup_string": "…",
    "show_shift_end_popup": false, "shift_end_popup_string": "…"
  },
  "supporting_page": { "term_condition_hcp_file": "https://…signed…", "faq_hcp_file": "https://…", … } | null,
  "dev_configuration": [ { "id": 1, "config_key": "age_limit", "config_value": "18" }, … ],
  "lms": { "id": 1, "lms_url": "…" } | null,
  "string_translation": { "shift_accept_popup_string": "…", "shift_end_popup_string": "…" }
}
```

- ⚠️ **All `image_path` values are now `""`.** Each supporting-page file field is already a complete signed URL, so `image_path.x + file` still produces a working link.
- ⚠️ `documents` lists only **active** document types.
- ⚠️ `supporting_page` can be `null` if the organisation hasn't set any up.
- ⚠️ `configuration` holds only the fields listed above. v2 sent the whole settings row.

### `POST /get-total-shifts-and-hours-completed`
- Success: `{"completed_shifts": 30280, "completed_hours": "245678.5 Hours"}` with message `Record found.`
- If there are none: `status:false`, `Record not available.`
- ⚠️ This now counts completed and approved shifts. v2 was counting cancelled shifts because of a bug.

---

## 6. Session (token required)

| Endpoint | Body | Success |
|---|---|---|
| `verify-token` | `token?` | `{"_status": true, "_message": "Auth token verified.", "_data": {"access_token": "…"}}` (v2's underscore keys) |
| `logout` | — | `"You have been logged out."`, `data: null`. Also stops push notifications to this device. |

---

## 7. HCP endpoints — `/api/v2/hcp/...` (token required)

Every endpoint acts **only on the signed-in HCP**. There is no way to read or change another HCP. Unless shown otherwise, successful writes return `data: []`.

### Profile and onboarding

| Endpoint | Body | Success message → `data` |
|---|---|---|
| `hcp/profile` | — | `Hcp found.` → `{hcp: {…all hcp fields, image_url, cv_url, hcp_block_times_count, average_rating, user, country, state, city, suburb, documents, hcp_block_times[{…, user:{id,name}}], hcp_professions[…]}}` |
| `hcp/dashboard` | — | `Dashboard found.` → see below |
| `hcp/personal-details` | **required:** `first_name`, `last_name`, `country` (id), `state` (id), `address`, `latitude`, `longitude`. **optional:** `city`, `suburb`, `post_code`, `about_me`, `contact_number`, `maximum_distance`, `date_of_birth`, `gender` (male/female/other), `accept_lower_level_job` (0/1), `image` (file, image, ≤ 5 MB) | `Hcp saved successfully.` |
| `hcp/update-registration-number` | **required:** `tfn_number`, `cv` (a file, **or** the current CV filename as text to keep it). **optional:** `registration_number`, `abn_number`, `accept_lower_level_job` | `Hcp saved successfully.` |
| `hcp/update-account-details` | **required:** `bank_name`, `branch_name_and_address`, `account_name`, `account_number`, `bsb_number`. **optional:** `superannuation_detail_fund_name`, `superannuation_detail_membership_no` | `Hcp saved successfully.` |
| `hcp/update-thank-you-screen-status` | — | `Hcp saved successfully.` |
| `hcp/update-available-job-status` | `status` (0/1 or true/false) | `Job Status updated successfully.`. Sending the same value as now → `status:false`, `Unable to update Job Status, Please try again!.` (as v2) |
| `hcp/update-app-registration-screen` | `app_registration_screen` | `App registration screen updated successfully.`. See the note below. |
| `hcp/update-device-id` | `device_id` | `Device Id updated successfully.`, `data: null` |
| `hcp/change-password` | `current_password`, `new_password` (min 8), `new_password_again` (must match) | `{"status":true,"message":"Password updated successfully."}` (**no `data` key**, as v2) |

⚠️ **Changed:**
- **Omitted optional fields are left unchanged.** v2 cleared them to empty, including wiping the CV when no file was re-sent.
- **`change-password` now checks `current_password`.** A wrong value returns 422 with `errors.current_password: ["Current password is incorrect."]`. Reusing the current password returns `errors.new_password`.
- **The registration screen is calculated by the server** from the HCP's real details and documents:
  - `1` = personal details missing
  - `2` = a profession document missing or rejected
  - `3` = a general document missing or rejected
  - `0` = complete, awaiting approval

  `update-app-registration-screen` only triggers that recalculation. Sending `0` while something is still missing returns `status:false`, `Unable to update App registration screen, Please try again!.`. Read the real value from `hcp.app_registration_screen` in `profile` or `login`.
- Personal details count as complete **without `gender`**. It's accepted but optional.

#### `hcp/dashboard` data (v2 key order)

```json
{
  "shift": { next shift object — see §8 "Shift object" } | null,
  "available_shifts": 3, "upcoming_shifts": 1, "scheduled_shifts": 4,
  "current_fortnight": 812.5, "current_week": 406.25, "current_year": 15230.75,
  "hcp_current_point": 0,
  "hcp_messages": { "point_message": null, "referral_and_earn": "" },
  "fortnight_start_date": "2026-09-24T09:01:00.000000Z", "fortnight_end_date": "2026-10-01T09:00:00.000000Z",
  "week_start_date": "…", "week_end_date": "…",
  "payroll_frequency": "weekly", "payroll_cutoff_day": "thursday", "payroll_cutoff_time": "17:00:00",
  "period_start": "…", "period_end": "…",
  "pending_profession_documents": [ { "hcp_profession_id": 1, "category_id": 2, "profession_id": 7, "profession_name": "…", "level_id": 3 } ]
}
```

⚠️ **Changed:**
- Earnings come from payroll:
  - `current_fortnight` is the current pay period.
  - `current_week` is the week of that period you're in.
  - `current_year` is year to date.

  v2 showed the week total as the "fortnight" figure and showed 0 all weekend.
- Points and referrals are retired. `point_message` is always `null` and `referral_and_earn` is always `""`. Hide those widgets when they're empty.
- `payroll_cutoff_time` is zero-padded (`17:00:00`). v2 sent `17:0:0`.

### Documents

| Endpoint | Body | Success |
|---|---|---|
| `hcp/documents` | `type` (`general-statement` / `duty-statement` / `profession`), `page?` | `Document found.` → `{hcps: paginator}`. **The key is `hcps`**, as in v2. Each item has `document` and ⚠️ **new `document_url`** (signed). ⚠️ One item per certification (held per person): `profession`, `level`, `profession_id`, `level_id` are gone; **`required_by`** lists the worker's professions that require it. |
| `hcp/add-update-document` | **required:** `document_id`, `document_name`, `document_file` (a file, or the current filename as text to only update the expiry). **optional:** `expiry_date` | `Document saved successfully.` |
| `hcp/add-update-profession-document` | **required:** `profession_id`, `document_id`, `document_name`, `document_file`. **optional:** `level_id` (ignored), `expiry_date` | `Profession document uploaded saved successfully.` `profession_id` is checked (the certification must be configured for it) but not stored. |

- Files may be pdf, jpg, jpeg, png, doc or docx, up to 10 MB.
- Failures return `status:false`, `Unable to save Document, Please try again!.` or `Unable to save Profession document upload, Please try again!.`.

⚠️ **Changed (the upload is refused when any of these is true):**
- the document type isn't an active one of the right kind (general vs profession);
- for profession documents, the HCP doesn't hold that profession, or the document isn't required for it;
- it's a first upload with no file.

A replacement upload goes back for admin approval as a re-approval.

### Professions

| Endpoint | Body | Success |
|---|---|---|
| `get-professions` (no `hcp/` prefix) | `category_id?` | `Profession found.` → `{professions: [...]}`: active professions the HCP **doesn't already hold**, each with `document` and `document_configuration[].document` |
| `hcp/get-hcp-professions` | — | `Professions found.` → `{professions: [ {…, category, level, profession: {…, profession_documents_count, mandatory_documents_count, uploaded_mandatory_documents_count}} ]}` |
| `hcp/get-profession-documents` | `profession_id` | `Professions Documents found.` → `{professions: [ {…, profession_documents: [ {…, document: {…, hcp_document: {…, document_url} \| null}} ]} ]}` |
| `hcp/update-hcp-profession-status` | `hcp_profession_id`, `status` | `Profession status updated successfully.` |
| `hcp/delete-hcp-profession` | `hcp_profession_id` | `Profession deleted successfully.`. This also deletes that profession's uploaded documents. |

⚠️ **Changed:**
- An HCP can only switch **their own** professions between `active` and `inactive`. Any other status, or another HCP's profession, returns `status:false`, `Unable to update Profession status, Please try again!.`. In v2 anyone could approve or delete anyone's professions.
- Deleting a profession that isn't yours returns `Unable to delete Profession, Please try again!.`.
- ⚠️ Adding professions from the app (`hcp/add-update-profession`, `-new`) is **removed**. Professions now come from recruitment (Hiresome) and admins.

### Notifications and ratings

| Endpoint | Body | Success |
|---|---|---|
| `hcp/notifications` | `page?` | `Notification found.` → `{hcps: paginator}` (**key `hcps`**, as in v2). Items have `shift: {id, shift_prefix, shift_type} \| null`. |
| `hcp/ratings` | `facility?`, `date_range?` (`"2026-01-01 to 2026-03-31"`), `facility_search?`, `page?` | `Ratings found.` → `{ratings: paginator}`, each item with `shift`, `category`, `profession`, `facility`, `hcp` |

- Empty lists return `Notification not available.` or `Ratings not available.`.
- ⚠️ Notifications no longer include expired "all HCPs" broadcasts, which v2 showed by mistake.

### Contact verification (email / mobile)

| Endpoint | Body | Success |
|---|---|---|
| `hcp/send-email-verification` | `email` (must not belong to **another** account) | `We have sent to you a mail for email verification, …` → `{request_token}` |
| `hcp/verify-email` | `otp` (6 digits), `request_token` | `Email verify successfully` |
| `hcp/send-mobile-verification` | `contact_number` (E.164, e.g. `+61412345678`) | `We have sent to you a sms for contact number verification, …` → `{request_token}` |
| `hcp/verify-mobile` | `otp`, `request_token` | `Contact number verify successfully` |

⚠️ **Changed (important):**
- **The code is no longer in the send response.** `data.otp` is gone; the user must enter the code they received.
- The endpoints now **require login**. v2 had them public for self-signup, which is removed.
- A code expires after **10 minutes**, allows **5** wrong guesses, and works **once**.
- Only the newest code sent to an address is valid.
- An address can receive at most 5 codes per hour. Over the limit: `status:false`, `Too many codes requested. Please try again later.`
- A wrong or expired code returns `status:false`, `Invalid OTP.`.
- If SMS isn't set up for the organisation, `send-mobile-verification` returns `status:false`, `Oops! SMS service not working.`. v2 claimed success even when nothing was sent.

---

## 8. Shift endpoints — `/api/v2/shift/...` (token required)

Shift status codes (`shift_status`, a string): `"0"` open, `"1"` scheduled (assigned), `"2"` running, `"3"` cancelled, `"4"` completed (awaiting approval), `"6"` approved.

### `POST /shift`: lists

| Body | Notes |
|---|---|
| `shift_status` (required) | One of the values in the table below |
| `filter_start_date`, `filter_end_date` (optional) | Both are needed for the date filter to apply; used by `completed`, `cancelled`, `transferred` |
| `page` | optional |

| `shift_status` | `data.shifts` |
|---|---|
| `upcoming` | `{upcoming_shifts: P, running_shifts: P}`. Scheduled shifts starting within 12 h (including overdue ones), plus running shifts |
| `available` | `{available_shifts: P, transfer_shifts: P, swap_shifts: P}` |
| `completed` | `{approved_shifts: P, completed_shifts: P}`, newest first |
| `scheduled` / `running` | a single paginator |
| `cancelled` | a single paginator of shifts **this HCP** cancelled |
| `transferred` | a single paginator of shifts an admin moved off this HCP |

- `P` is a paginator.
- Success message: `Shifts found.`. When every list is empty: `status:false`, `Shifts not available.`.

⚠️ **Changed:**
- **An unrecognised `shift_status` returns "Shifts not available."** v2 returned every shift in the organisation.
- **`available`** now uses exactly the rules used to notify HCPs of new shifts and to accept them:
  - the HCP holds the active profession and exactly the shift's level (accepting lower levels is retired);
  - the site is within the broadcast radius;
  - the HCP isn't blocked by the facility;
  - no clash with their own shifts;
  - the shift hasn't ended;
  - the HCP hasn't declined it.

  So anything listed can normally be accepted. v2 required an old "buddy shift" approval (now retired) and often showed nothing.
- `transfer_shifts` and `swap_shifts` are always empty. Swaps and transfer requests are retired.

### Shift object (list items and `shift/details`)

This is every `shifts` column **except** facility-side money and internal fields. ⚠️ Removed: `facility_amount`, `admin_amount`, `admin_fees`, `facility_per_rate`, `total_admin_fees_with_loading`, `facility_shift_loading`, `facility_shift_total_loading`, `facility_sleepover_rate`, the `sleepover_*_admin_fees` and `sleepover_*_faciliy_amount` fields, `notes_internal`, `booked_by`, `deleted_at`. The HCP's own pay fields (`hcp_amount`, `hcp_per_rate`, `hcp_shift_loading`, …) are still there.

Relations:

```json
"hcp": {id, first_name, last_name, average_rating} | null,
"category": {id, name}, "profession": {id, name}, "level": {id, name} | null,
"facility": {id, name, facility_group_id, address, latitude, longitude, average_rating} | null,
"client": {id, first_name, last_name, address, suburb_name, latitude, longitude} | null,
"cancelled_shift": {…} | null,          // this HCP's own cancellation row
"shift_transfer_from": {…, transfer_hcp:{id,first_name,last_name,image}} | null,
"shift_transfer_to": {…} | null,
"rejected_shifts": [ … ],               // this HCP's decline row(s)
"state": {… incl. time_zone_name}
```

- ⚠️ **New:** `client`. A shift can be at a **client's home** instead of a facility. Then `facility` is `null`, and `client` holds the address and coordinates.
- ⚠️ `shift_swap` and `buddy_shift_request` are no longer included.

### `POST /shift/details`
- Body: `shift_id`.
- Success: `Shift found.` → `{shift: {…shift object, buddy_hcp, rating_reviews, hcp_rating, facility_rating, fc_doc_id, facility_duty_document, fc_doc_profession_id}}`.
- Shown only when the shift is the HCP's own, they previously cancelled it or were moved off it, or it's open and they could accept it.
- Failures:
  - Someone else's shift: `This shift already has been applied by another hcp.`
  - Missing: `Shift not available.`

### Actions

| Endpoint | Body | Success message | Typical failures (`status:false`, HTTP 200) |
|---|---|---|---|
| `shift/accept` | `shift_id` | `Shift applied successfully` | `Ooops! This shift has already been accepted by another HCP. Sorry!` · `Only pending shifts can be accepted or assigned.` · `This HCP account is pending document approval.` · `This HCP does not have the required profession on their profile.` · `This HCP already has an overlapping shift.` · `This HCP has reached the maximum allowed hours for this day.` / `…fortnight.` · `This shift has already ended.` |
| `shift/reject` | `shift_id` | `Shift rejected successfully` | `This shift has been already rejected.` · `Only open shifts can be declined.` |
| `shift/start` | `shift_id`, `latitude?`, `longitude?` | `Shift started successfully` | `Shift not available.` (not yours) · `Only scheduled shifts can be started.` · `You are 2.4 km from the shift location -- must be within 1 km to start.` · `You can only start the shift within 15 minutes of the scheduled start time.` · `No location found for this shift. Please ensure location services are enabled.`. Failures return `data: []`, as in v2. |
| `shift/end` | `shift_id` | `Shift ended successfully` | `Shift not available.` (not yours) · `Only running shifts can be ended.` |
| `shift/shift-tracking` | `shift_id`, `latitude`, `longitude` (`facility_id` is optional and ignored) | `Shift Tracking created successfully.` → `data: []`, **or** on arrival `{"hcp_reached": true, "shift_id": 123, "distance": 42.1, "time": "2026-09-28T01:05:00.000000Z"}` (`distance` in **metres**) | `Shift not available.` (not yours, or not scheduled/running) |
| `shift/give-rating` | `shift_id`, `rating` (1–5), `comment` | `Rating created successfully.` | `Unable to create Rating, Please try again!.` (already rated, or not a completed facility shift) · `Shift not available.` (not yours) |

⚠️ **Changed:**
- `start`, `end`, `shift-tracking` and `give-rating` only work on **the HCP's own shift**. v2 allowed any shift.
- If `start` gets no coordinates, it falls back to the last tracking ping for the shift.
- **`reject` now removes the shift** from that HCP's `available` list, and only works on open shifts.
- **`shift-tracking` arrival** now reports correctly, including on the first ping. v2 compared metres against a kilometre setting and never reported arrival on the first ping.
- **`give-rating` works now.** v2 always returned a 500. It rates the **facility** and is allowed once per completed or approved shift. The facility, category and profession come from the shift; `facility_id`, `category_id` and `profession_id` in the body are accepted but ignored.
- Removed endpoints: `shift/cancel`, all transfer and swap endpoints, buddy-shift endpoints, `shift/payments`, `shift/payment-details`.

---

## 9. Facilities (token required)

### `POST /facility`
- Body: `state_id?`, `name?` (partial match).
- Success: `Facility found.` → `{facilities: [ {id, facility_prefix, name, facility_name_id, facility_group_id, address, city_name, suburb_name, post_code, country_id, state_id, city_id, suburb_id, latitude, longitude, status, average_rating, country, state, city, suburb, facility_name} ]}`.
- ⚠️ Contact details, emails, and card/Stripe fields are **no longer included**. v2 exposed them.

---

## 10. Files — ⚠️ Changed

Files are now in private storage. **Plain filenames can't be turned into links any more**, so use the new signed URL fields:

| Where | Use |
|---|---|
| Profile photo | `hcp.image_url` (login, `hcp/profile`) |
| CV | `hcp.cv_url` |
| HCP documents | `document_url` on each item of `hcp/documents`, and on `hcp_document` in `hcp/get-profession-documents` |
| Terms / FAQ / policy pages | the fields in `supporting_page` are already full URLs |

- Signed URLs **expire after about 12 hours**. Refetch the profile or list to get fresh ones; don't cache them long-term.
- The old filename fields (`image`, `cv`, `document_name`) are still sent, for display only.

---

## 11. Checklist for the app team

Please check each of these against the app:

1. **Base path** `/api/…` → `/api/v2/…`.
2. **Organisation selection:** email → `tenancy/resolve` → `X-Tenant-Code` header on every call (§2).
3. **Signed file URLs:** use `image_url`, `cv_url` and `document_url`. `image_path.*` is now `""` (§10).
4. **Verification codes are not returned** by `send-*-verification`. The user must type the code (§7).
5. **`change-password` checks `current_password`** (§7).
6. **Registration screen is calculated by the server.** Read `app_registration_screen` from `profile`/`login` rather than assuming the requested value was saved (§7).
7. **Profession status:** only `active` ↔ `inactive`, own professions only (§7).
8. **Shift lists:**
   - unknown `shift_status` → "not available";
   - `transfer_shifts`/`swap_shifts` always empty;
   - declined shifts disappear from `available` (§8).
9. **Shift object:**
   - facility-side money and `notes_internal` removed;
   - new `client` relation for home-care shifts, where `facility` can be `null` (§8).
10. **Dashboard:** `point_message` always `null`, `referral_and_earn` always `""`, earnings figures redefined (§7).
11. **Removed endpoints** must not be called:
    - `register`, `guest-login`
    - `hcp/register`, `hcp/add-update-profession`, `hcp/add-update-profession-new`
    - `hcp/refer-and-earn`, `hcp/get-my-buddy-shift`, `hcps/`
    - `shift/cancel`, `shift/transfer`, the transfer accept/reject/cancel endpoints and all swap endpoints
    - `shift/create-buddy-shift-request`, `shift/buddy-shifts`, `shift/payments`, `shift/payment-details`
12. **401** bodies are now `{"status":false,"message":"Unauthenticated."}`.

If a screen breaks and the cause isn't listed here, send the backend team the request (endpoint, body, `X-Tenant-Code`) and the full response.
