# API Documentation

## Base URL

```
http://localhost:8000/api
```

## Authentication

- **Method**: Laravel Sanctum (Bearer token)
- **Header**: `Authorization: Bearer <token>`
- **Token obtained from**: `POST /api/login`
- **Token expiration**: `SANCTUM_TOKEN_EXPIRATION` env var, minutes (default 43200 / 30 days)
- **Unauthenticated response**: `{"success": false, "message": "Unauthenticated."}` (HTTP 401)

## Rate Limiting

- All `api` routes: 60 requests/minute, keyed by authenticated user id (or IP if unauthenticated).
- `/api/register`, `/api/register/direct`, `/api/register/verify`, `/api/app-version/create-direct`,
  `/api/login`: additional `register` limiter, 6 requests/minute per IP.
- OTP send/verify (`AuthController`, `ClientAuthController`): additional in-code limiter, 5 attempts per
  10 minutes, keyed by phone number.
- Exceeding a limit returns HTTP 429.

## Response Format

All endpoints return JSON with the same envelope:

```json
{
    "success": true|false,
    "message": "Human-readable message",
    "data": { ... } | null,
    "errors": { ... } | null  // Only on validation errors
}
```

## Naming Convention

All authenticated routes use `POST` method with a verb-path pattern:
`POST /api/{module}/{action}`

Exceptions: File downloads use `GET`.

## Public Endpoints

| Method | Path | Controller | Purpose |
|--------|------|-----------|---------|
| POST | `/api/register` | RegistrationController@register | Send registration OTP |
| POST | `/api/register/verify` | RegistrationController@verifyAndRegister | Verify OTP and create user |
| POST | `/api/login` | AuthController@login | Login, returns Sanctum token |
| GET | `/api/google/callback` | GoogleAuthController@handleCallback | Google OAuth callback |
| GET | `/api/config/push/vapid-key` | ConfigController@pushVapidKey | Web push: returns the web Firebase project's VAPID public key (not a secret) for the browser frontend |
| GET | `/api/cron/schedule` | CronController@schedule | Run the task scheduler (HTTP cron trigger). Requires `CRON_SECRET` via `token` query param or `X-Cron-Token` header. See [deployment.md](deployment.md#http-triggered-cron-cpanel--shared-hosting) |
| GET | `/api/cron/queue` | CronController@queue | Drain pending queue jobs (HTTP cron trigger). Same `CRON_SECRET` auth as above |
| GET | `/ivr/call/incoming/upload` | PhoneCallLogController@upload_ivr_incoming_call | IVR incoming call upload |
| GET | `/ivr/call/outgoing/upload` | PhoneCallLogController@upload_ivr_outgoing_call | IVR outgoing call upload |
| GET | `/google/auth` | GoogleAuthController@redirectToGoogle | Google OAuth redirect |
| GET | `/webhook/ivr/lead` | LeadController@webhook_ivr_lead_capture | IVR lead webhook |
| GET | `/webhook/justdial/lead` | LeadController@webhook_justdial_lead_capture | JustDial lead webhook |
| GET | `/webhook/website/lead` | LeadController@webhook_website_lead_capture | Website form lead webhook |
| GET | `/webhook/meta/lead` | Meta\MetaWebhookController@verify | Meta webhook subscription verification (`hub_challenge` handshake) |
| POST | `/webhook/meta/lead` | Meta\MetaWebhookController@receive | Meta (Facebook/Instagram) Lead Ads webhook delivery. Verified via `X-Hub-Signature-256` against the active `MetaConnection.app_secret`, not Sanctum — Meta has no CRM user token. Logs to `ad_webhook_logs` and dispatches `ProcessMetaLead` per leadgen event; does not call the Graph API inline |
| GET | `/meta/connect` | Meta\MetaAuthController@redirect | Meta OAuth redirect (route structure only, 501 until real App credentials are wired) |
| GET | `/meta/callback` | Meta\MetaAuthController@callback | Meta OAuth callback (route structure only, 501 until real App credentials are wired) |

Removed: the previous `/webhook/facebook/lead` and `/webhook/instagram/lead` GET stub routes
(`LeadController@webhook_facebook_lead_capture`/`webhook_instagram_lead_capture`) were placeholders
that expected a pre-flattened `name/email/phone` payload with no `leadgen_id`, no Graph API fetch,
and no signature verification — they could not work with real Meta Lead Ads and have been replaced
by the `/webhook/meta/lead` pair above.

## Authenticated Endpoints

### Account Validation (All auth routes pass through `account_validation` middleware first)

| Method | Path | Controller | Purpose |
|--------|------|-----------|---------|
| POST | `/api/verify` | AuthController@verify | Verify account |
| POST | `/api/logout` | AuthController@logout | Logout (revoke token). Optional `platform` (`mobile`\|`web`, default `mobile`) clears only that platform's FCM token |
| POST | `/api/task/assignees/backfill` | TaskController@backfill_task_assignees | One-time data-migration: copies legacy `tasks.assigned_to` into `task_assignees`. Idempotent. Requires `task_management.task.override` permission. |
| POST | `/api/sales/payment-status/backfill` | LeadSaleController@backfill_sale_payment_status | One-time data-migration: recomputes `payment_status` for `lead_sales` rows predating `syncPaymentStatus()`. Idempotent, upgrade-only. Requires `report_management.sales.override` permission. |

### User Management

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/user/create` | Create user |
| POST | `/api/user/all/fetch` | List all users |
| POST | `/api/user/details/fetch` | Get user details |
| POST | `/api/user/own/data` | Get own user data |
| POST | `/api/user/own/image/update` | Update own profile image |
| POST | `/api/user/own/image/delete` | Delete own profile image |
| POST | `/api/user/personal/update` | Update personal info |
| POST | `/api/user/address/update` | Update address |
| POST | `/api/user/bank/update` | Update bank details |
| POST | `/api/user/professional/update` | Update professional details |
| POST | `/api/user/schedule/update` | Update work schedule |
| POST | `/api/user/integration/update` | Update integrations |
| POST | `/api/user/device_token/update` | Update FCM device token. Optional `platform` (`mobile`\|`web`, default `mobile`) selects `fcm_token` vs `web_fcm_token` |
| POST | `/api/user/impersonate` | Impersonate another user |
| POST | `/api/user/impersonate/leave` | Leave impersonation |
| POST | `/api/user/status/update` | Update user status |
| POST | `/api/user/search` | Search users |
| POST | `/api/user/kyc` | KYC verification |
| POST | `/api/user/document/create` | Create user document |
| POST | `/api/user/document/delete` | Delete user document |
| POST | `/api/user/asset/create` | Create asset assignment |
| POST | `/api/user/asset/return` | Return asset |
| POST | `/api/user/kpi/update` | Update KPI targets |
| POST | `/api/user/kpi/current/fetch` | Fetch current KPI |
| POST | `/api/user/analytics/fetch` | Fetch user analytics |

### Lead Management

#### Company (Lead's Organization)

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/company/create` | Create company |
| POST | `/api/company/update` | Update company |
| POST | `/api/company/active/fetch` | List active companies |
| POST | `/api/company/inactive/fetch` | List inactive companies |
| POST | `/api/company/delete` | Delete company |
| POST | `/api/company/status/update` | Update status |
| POST | `/api/company/search` | Search companies |

#### Leads

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/lead/create` | Create lead |
| POST | `/api/lead/update` | Update lead |
| POST | `/api/lead/delete` | Delete lead |
| POST | `/api/lead/all/fetch` | List all leads |
| POST | `/api/lead/raw/all/fetch` | List raw leads |
| POST | `/api/lead/verified/all/fetch` | List verified leads |
| POST | `/api/lead/client/all/fetch` | List clients |
| POST | `/api/lead/all/search` | Search leads |
| POST | `/api/lead/note/create` | Create note |
| POST | `/api/lead/notes/fetch` | Fetch notes |
| POST | `/api/lead/follow-up/create` | Create follow-up |
| POST | `/api/lead/follow-ups/fetch` | Fetch follow-ups |
| POST | `/api/lead/follow-ups/all/fetch` | Fetch all follow-ups |
| POST | `/api/lead/verified/convert` | Convert to verified |
| POST | `/api/lead/not_interest/convert` | Mark not interested |
| POST | `/api/lead/call_back/convert` | Mark call back |
| POST | `/api/lead/single/transfer` | Transfer single lead |
| POST | `/api/lead/bulk/transfer` | Bulk transfer leads |
| POST | `/api/lead/bulk/delete` | Bulk delete leads |
| POST | `/api/lead/single/share` | Share lead |
| POST | `/api/lead/priority/update` | Update priority |
| POST | `/api/lead/phone/primary/set` | Set primary phone |
| POST | `/api/lead/email/primary/set` | Set primary email |
| POST | `/api/lead/claim` | Claim lead |
| POST | `/api/lead/analytics` | Lead analytics |
| POST | `/api/lead/report` | Client-wise comprehensive report (single lead, all related data, any status) |
| GET | `/api/lead/export` | Export leads (Excel) |
| POST | `/api/lead/import/template` | Download the import template (Excel) |
| POST | `/api/lead/import/preview` | Read headers + sample rows + distributable row count (before mapping) |
| POST | `/api/lead/import/analyze` | Profile the file against a chosen column mapping: per-field formats, counts, and special-character anomalies |
| POST | `/api/lead/import/with_mapping` | Import leads with column mapping, per-field format normalization, required-field skipping, anomaly handling, and user distribution |
| POST | `/api/lead/import/reports/fetch` | List past import runs (paginated summaries) for the account |
| POST | `/api/lead/import/report/detail` | Fetch one import report with full config + per-row error/skip/warning detail (`report_id`) |

See [Lead Import (smart mapping) flow](#lead-import-smart-mapping-flow) below for request/response detail.

#### Lead Import (smart mapping) flow

A three-step flow for importing leads from an Excel/CSV file with column mapping,
data-quality review, per-field cleanup, and user distribution. All three require
`lead_management.raw_lead.create` (or `.override`). Files accept `xlsx`, `xls`,
`csv` up to 20 MB. Header names are slugged (lowercase, snake_case) by the
reader; `column_mapping` values reference those slugged header keys.

**1. `POST /api/lead/import/preview`** — inspect the file before mapping.

Request: `file` (multipart).

```json
{
  "success": true,
  "data": {
    "headers": ["name", "phone", "email"],
    "sample_data": [ { "name": "John", "phone": "9876543210" } ],
    "available_fields": { "lead_fields": {}, "company_fields": {} },
    "total_rows": 250,
    "row_counts": { "total": 254, "blank": 3, "header": 1, "data": 250 }
  }
}
```

`total_rows` is the distributable count (`row_counts.data`): total data rows minus
fully-blank rows and any mid-file rows that repeat the header labels.

**2. `POST /api/lead/import/analyze`** — profile the file against a mapping.

Request: `file` (multipart) + `column_mapping` (`system_field => header_key`).

Per mapped field the response reports the value formats present (with a real
example and count each), filled/empty/distinct counts, sample values, the
selectable normalization presets (`available_formats`, each with a stable `key`
and machine-usable `spec`), a `custom_format_template`, and a special-character
`anomalies` block for string fields.

```json
{
  "success": true,
  "data": {
    "total_rows": 250,
    "row_counts": { "total": 254, "blank": 3, "header": 1, "data": 250 },
    "fields": {
      "phone": {
        "excel_header": "phone",
        "filled": 248, "empty": 2, "distinct": 246,
        "samples": ["+91 98765 43210", "9876543210"],
        "detected_formats": [
          { "label": "Plain digits, 10 digits", "count": 190, "example": "9876543210" },
          { "label": "With country code (+), Formatted (separators), 12 digits", "count": 58, "example": "+91 98765 43210" }
        ],
        "available_formats": [
          { "key": "plain_10", "label": "Plain 10 digits (9876543210)", "spec": { "strip_separators": true, "strip_leading_zeros": true, "country_code": "drop", "expected_digits": 10 } }
        ],
        "custom_format_template": { "strip_separators": true, "strip_leading_zeros": false, "country_code": "keep", "force_country_code": "", "expected_digits": null },
        "anomalies": { "checked": false }
      },
      "name": {
        "excel_header": "name",
        "filled": 250, "empty": 0, "distinct": 249,
        "samples": ["John Doe"],
        "detected_formats": [ { "label": "Text", "count": 250, "example": "John Doe" } ],
        "available_formats": [ { "key": "title_case", "label": "Title Case", "spec": { "trim": true, "collapse_spaces": true, "case": "title" } } ],
        "custom_format_template": { "trim": true, "collapse_spaces": true, "case": "none" },
        "anomalies": { "checked": true, "affected_rows": 12, "chars": ["@", "1", "2"], "examples": ["Ann@Marie", "Bob123"] }
      }
    }
  }
}
```

**3. `POST /api/lead/import/with_mapping`** — perform the import.

Request:

```json
{
  "file": "<multipart>",
  "column_mapping": { "name": "name", "phone": "phone", "email": "email" },
  "field_rules": {
    "phone": { "format_key": "plain_10" },
    "name":  { "on_anomaly": "fix", "required": true },
    "email": { "format": { "trim": true, "case": "lower" }, "on_anomaly": "ignore" }
  },
  "created_by_distribution":  { "mode": "percentage", "users": [ { "user_id": 5, "value": 60 }, { "user_id": 6, "value": 40 } ] },
  "assigned_to_distribution": { "mode": "count", "users": [ { "user_id": 7, "value": 100 } ] }
}
```

`field_rules` is optional, keyed by system field. Each entry supports:
- `format_key` — pick an `available_formats` preset, **or** `format` — a custom spec.
- `required` — `true` skips (does not import) rows where this field is blank.
- `on_anomaly` — `fix` strips disallowed characters, `discard` skips the row,
  `ignore` (default) imports as-is. Applies to string fields only.

Distribution percentages must sum to 100; count-mode sums must not exceed the
distributable row count. Rules are applied per row in order: required-skip →
anomaly (fix/discard) → format normalization → validation/dedup → insert.

```json
{
  "success": true,
  "message": "Lead import with mapping completed",
  "data": {
    "report_id": 42,
    "total_imported": 240,
    "total_failed": 4,
    "total_skipped": 6,
    "total_normalized": 198,
    "errors": [ { "row": 12, "error": "Duplicate phone number: 9876543210 (existing lead ID: 88)" } ],
    "skips": [ { "row": 20, "reason": "Required field 'name' is blank" } ],
    "normalization_warnings": [ { "row": 33, "field": "phone", "warning": "Phone has 9 digits, expected 10", "value": "987654321" } ]
  }
}
```

Every import run is persisted as a **lead import report** (`report_id` above).

**`POST /api/lead/import/reports/fetch`** — paginated list of past runs (account-scoped).
Optional `per_page` (1–100, default 15) and `imported_by` filter. Each item has
`id`, `file_name`, `imported_by`, the summary counts, and `created_at`, plus a
`pagination` block (`current_page`/`last_page`/`per_page`/`total`/`next_page`).

**`POST /api/lead/import/report/detail`** — full detail for one report (`report_id`,
required). Returns `summary` counts, `row_counts`, the exact `column_mapping` /
`field_rules` / distribution used, and the complete `errors` / `skips` /
`normalization_warnings` arrays. Both endpoints require
`lead_management.raw_lead.create` (or `.override`) and are cross-account safe.

#### Lead Group Sub-routes (`/api/lead/group/*`)

Reusable, saved lead audiences (fixed snapshot membership) — see
[campaign-management.md §1.5](campaign-management.md#15-lead-groups--reusable-saved-audiences-built-in-the-leads-module)
for full detail. Primarily consumed as `lead_group_id` in `POST /api/campaign/select_lead_group`.

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/lead/group/create` | Create a group from an explicit `lead_ids` array |
| POST | `/api/lead/group/preview-filters` | Count how many leads a filter set would match, without creating a group — use before `create-from-filters` to show the user the resulting group size |
| POST | `/api/lead/group/create-from-filters` | Create a group from the leads matching a filter set (same filters as `/api/lead/all/fetch`) instead of explicit `lead_ids` |
| POST | `/api/lead/group/fetch` | Get one group (with members) or a paginated list |
| POST | `/api/lead/group/update` | Rename/redescribe a group |
| POST | `/api/lead/group/members/add` | Add more leads by `lead_ids` |
| POST | `/api/lead/group/members/remove` | Remove specific leads |
| POST | `/api/lead/group/delete` | Soft-delete a group |

#### Lead Sales Sub-routes (`/api/lead/sale/*`)

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/lead/sale/create` | Create lead sale |
| POST | `/api/lead/sale/incentive/update` | Manually override a sale's incentive amount |
| POST | `/api/incentive/slab/create` | Create a per-user incentive slab for a product |
| POST | `/api/incentive/slab/update` | Update an incentive slab |
| POST | `/api/incentive/slab/delete` | Soft-delete an incentive slab |
| POST | `/api/incentive/slab/fetch` | List incentive slabs (filter by `product_id`/`user_id`) |
| POST | `/api/lead/sale/update` | Update sale |
| POST | `/api/lead/sale/delete` | Delete sale |
| POST | `/api/lead/sale/payment/create` | Create payment |
| POST | `/api/lead/sale/payment/update` | Update payment |
| POST | `/api/lead/sale/payment/delete` | Delete payment |
| POST | `/api/lead/sale/aggregated/fetch` | Aggregated sales report |
| POST | `/api/lead/sale/individual/fetch` | Individual sales list — each row includes `payment_status`, `approval` (`status`, `approved_at`, `approved_by`) and `bill_raised` (`status`, `date`, `invoice_number`, `remarks`) |
| POST | `/api/lead/sale/reminder/fetch` | Payment reminders: all unpaid/partial sales, flagged `is_overdue` (last payment/sale date older than `credit_settings.overdue_window_days`) and `credit_term_exceeded` (company over credit limit beyond its credit term); optional `bucket=current\|overdue\|credit_exceeded` filter. Rows also include the `approval`/`bill_raised` data described above |

#### Lead Overview Sub-routes (`/api/lead/overview/*`)

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/lead/overview/sale/fetch` | Sale data |
| POST | `/api/lead/overview/quotation/fetch` | Quotation data |
| POST | `/api/lead/overview/meeting/fetch` | Meeting data |
| POST | `/api/lead/overview/visit/fetch` | Visit data |
| POST | `/api/lead/overview/demo/fetch` | Demo data |
| POST | `/api/lead/overview/project/fetch` | Project data |
| POST | `/api/lead/overview/task/fetch` | Task data |
| POST | `/api/lead/overview/call/log/fetch` | Phone + IVR call logs for the lead's phone numbers |
| POST | `/api/lead/overview/counts/fetch` | Counts of every overview metric (sales, quotations, meetings, visits, demos, projects, tasks, call logs, notes, follow_ups, ownership_history) for a lead in one call; visit/demo/project/task counts respect the same `*.read`/`*.override` permission checks as their individual fetch endpoints and are 0 when unauthorized. Notes/follow_ups/ownership_history counts have no permission gate, matching their existing fetch endpoints |

### Meeting

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/meeting/create` | Create meeting |
| POST | `/api/meeting/update` | Update meeting |
| POST | `/api/meeting/transfer` | Transfer active user |
| POST | `/api/meeting/reschedule` | Reschedule meeting |
| POST | `/api/meeting/cancel` | Cancel meeting |
| POST | `/api/meeting/delete` | Soft delete meeting |
| POST | `/api/meeting/arrived` | Mark arrived (within 30min window) |
| POST | `/api/meeting/started` | Mark started (with GPS) |
| POST | `/api/meeting/otp/send` | Send OTP to lead's phone |
| POST | `/api/meeting/otp/verify` | Verify OTP |
| POST | `/api/meeting/completed` | Mark completed |
| POST | `/api/meeting/active/fetch` | List active meetings |
| POST | `/api/meeting/completed/fetch` | List completed meetings |
| POST | `/api/meeting/cancelled/fetch` | List cancelled meetings |
| POST | `/api/meeting/overdue/fetch` | List overdue meetings |
| POST | `/api/meeting/search` | Search meetings |

### Demo

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/demo/create` | Create demo |
| POST | `/api/demo/update` | Update demo |
| POST | `/api/demo/delete` | Delete demo |
| POST | `/api/demo/all/fetch` | List demos |
| POST | `/api/demo/start` | Start demo |
| POST | `/api/demo/complete` | Complete demo |
| POST | `/api/demo/cancel` | Cancel demo |
| POST | `/api/demo/share` | Share demo |
| POST | `/api/demo/creator/update` | Update creator |
| POST | `/api/demo/reschedule` | Reschedule demo |
| POST | `/api/demo/responses/sync` | Sync attendee responses |

### Product

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/product/all/fetch` | List products |
| POST | `/api/product/create` | Create product |
| POST | `/api/product/update` | Update product |
| POST | `/api/product/delete` | Delete product |
| POST | `/api/product/search` | Search products |
| GET | `/api/product/export` | Export products (Excel) |

### Visit

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/visit/create` | Create visit |
| POST | `/api/visit/all/fetch` | List visits |
| POST | `/api/visit/search` | Search visits |
| POST | `/api/visit/delete` | Delete visit |

### Task

A task supports multiple assignees. `create`/`update` take an `assignees` array of user IDs
(`assignees.*` must `exist:users,id`) instead of a single `assigned_to` field, and every task
response returns an `assignees` array (`[{id, name, image_path}, ...]`) instead of a singular
`assignee` object. Any user in a task's `assignees` may complete it, log time against it, add a
subtask to it, or stop its recurrence — not just the original single assignee. The `assigned_to`
query param on `search`/`all/fetch` is unchanged as a *filter* (matches tasks where the given user
is any one of the assignees).

A task also supports Monday.com-style **subtasks** — a single-level checklist of items, each with
a `title`, optional `due_date`, single `assigned_to`, `is_completed` flag, `position` for
drag-reorder, and a `carry_forward_on_recurrence` flag. A task response includes them under
`subtasks`. Only a subtask's assignee can mark it complete, and doing so requires a `log`
(string) and `duration_seconds` (integer) in the same request — these are created as an
`EventLog` on the *parent task* (not a separate subtask log), then the subtask is marked
complete; both happen inside one DB transaction. The creator (or a user with
`task_management.task.override`) can delete a subtask. Subtasks cannot be added to or removed
from a completed or overdue task.

`carry_forward_on_recurrence` (boolean, default `false`) controls whether a subtask is regenerated
when its recurring task spawns its next occurrence — same as assignees/tags/products, which are
always copied. A subtask with the flag off is a one-off item scoped to that single occurrence.

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/task/create` | Create task |
| POST | `/api/task/update` | Update task |
| POST | `/api/task/delete` | Delete task |
| POST | `/api/task/complete` | Complete task |
| POST | `/api/task/search` | Search tasks |
| POST | `/api/task/all/fetch` | List all tasks |
| POST | `/api/task/log/create` | Create task log |
| POST | `/api/task/log/delete` | Delete task log |
| POST | `/api/task/user/log/fetch` | Fetch user logs |
| POST | `/api/task/user/log/create` | Create user log |
| POST | `/api/task/user/log/delete` | Delete user log |
| POST | `/api/task/subtask/create` | Create subtask (`task_id`, `title`, `assigned_to`, optional `due_date`, optional `carry_forward_on_recurrence` boolean, default false) |
| POST | `/api/task/subtask/complete` | Complete subtask (`task_id`, `subtask_id`, `log`, `duration_seconds`) — creates an event log on the parent task, then completes the subtask |
| POST | `/api/task/subtask/delete` | Delete subtask (`task_id`, `subtask_id`) |
| POST | `/api/task/subtask/reorder` | Reorder subtasks (`task_id`, ordered `subtask_ids` array) |
| POST | `/api/task/subtask/all/fetch` | List subtasks assigned to the current user |
| POST | `/api/task/recurrence/stop` | Stop recurrence |
| POST | `/api/task/recurrence/generate` | Manually run the full recurring-task generation pass right now (identical rules to the daily `GenerateRecurringTasks` job), instead of waiting for the scheduler. No params; processes all eligible recurring tasks. Requires `task_management.task.override` |

### Project

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/project/create` | Create project |
| POST | `/api/project/update` | Update project |
| POST | `/api/project/complete` | Complete project |
| POST | `/api/project/delete` | Delete project |
| POST | `/api/project/all/fetch` | List all projects |
| POST | `/api/project/active/fetch` | List active projects |
| POST | `/api/project/complete/fetch` | List completed projects |
| POST | `/api/project/task/fetch` | Project tasks |
| POST | `/api/project/media/upload` | Upload media |
| POST | `/api/project/media/delete` | Delete media |
| POST | `/api/project/media/fetch` | List media |
| POST | `/api/project/ticket/create` | Create ticket |
| POST | `/api/project/ticket/update` | Update ticket |
| POST | `/api/project/ticket/delete` | Delete ticket |
| POST | `/api/project/ticket/fetch` | List tickets |

### Attendance

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/attendance/check_in` | Check in (with image, GPS) |
| POST | `/api/attendance/check_out` | Check out (with image, GPS) |
| POST | `/api/attendance/report/fetch` | Aggregated attendance report |
| POST | `/api/attendance/detailed/report/fetch` | Detailed report |
| POST | `/api/attendance/mark` | Manual mark attendance |
| POST | `/api/attendance/today/own/fetch` | Today's own attendance |
| POST | `/api/attendance/location/track` | Track GPS location |
| POST | `/api/attendance/location/history` | Location history |
| POST | `/api/attendance/backfill` | Backfill attendance |

### Shift

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/shift/create` | Create shift |
| POST | `/api/shift/update` | Update shift |
| POST | `/api/shift/all/fetch` | List shifts |
| POST | `/api/shift/search` | Search shifts |
| POST | `/api/shift/delete` | Delete shift |

### Tag

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/tag/create` | Create tag |
| POST | `/api/tag/update` | Update tag |
| POST | `/api/tag/delete` | Delete tag |
| POST | `/api/tag/all/fetch` | List tags |
| POST | `/api/tag/search` | Search tags |

### Quotation

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/quotation/create` | Create quotation |
| POST | `/api/quotation/update` | Update quotation |
| POST | `/api/quotation/delete` | Delete quotation |
| POST | `/api/quotation/all/fetch` | List quotations |
| POST | `/api/quotation/detail/fetch` | Quotation details |
| POST | `/api/quotation/draft/create` | Create draft |
| POST | `/api/quotation/draft/update` | Update draft |
| POST | `/api/quotation/draft/delete` | Delete draft |
| POST | `/api/quotation/draft/all/fetch` | List drafts |
| POST | `/api/quotation/draft/detail/fetch` | Draft details |
| POST | `/api/quotation/draft/finalize` | Finalize draft |
| POST | `/api/quotation/send-email` | Send via email |
| POST | `/api/quotation/email-history` | Email history |
| POST | `/api/quotation/resend-email` | Resend email |
| GET | `/api/quotation/pdf` | Download PDF |
| POST | `/api/quotation/term/create` | Create term |
| POST | `/api/quotation/term/update` | Update term |
| POST | `/api/quotation/term/delete` | Delete term |
| POST | `/api/quotation/term/fetch` | List terms |
| POST | `/api/quotation/email/body/update` | Update email body |
| POST | `/api/quotation/email/body/fetch` | Fetch email body |

### Leave

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/leave/type/create` | Create leave type |
| POST | `/api/leave/type/all/fetch` | List leave types |
| POST | `/api/leave/type/search` | Search leave types |
| POST | `/api/leave/type/update` | Update leave type |
| POST | `/api/leave/type/delete` | Delete leave type |
| POST | `/api/leave/entitlement/create` | Create entitlement |
| POST | `/api/leave/entitlement/update` | Update entitlement |
| POST | `/api/leave/application/create` | Apply leave |
| POST | `/api/leave/application/update` | Update application |
| POST | `/api/leave/application/approve` | Approve leave |
| POST | `/api/leave/application/reject` | Reject leave |
| POST | `/api/leave/application/pending/fetch` | Pending approvals |
| POST | `/api/leave/application/approved/fetch` | Approved applications |
| POST | `/api/leave/application/rejected/fetch` | Rejected applications |
| POST | `/api/leave/application/own/fetch` | Own approvals |

### Holiday

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/holiday/create` | Create holiday |
| POST | `/api/holiday/update` | Update holiday |
| POST | `/api/holiday/delete` | Delete holiday |
| POST | `/api/holiday/all/fetch` | List holidays |
| POST | `/api/holiday/by-year` | Holidays by year |
| POST | `/api/holiday/current-year` | Current year holidays |
| POST | `/api/holiday/year-options` | Year type options |

### Payroll

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/payroll/create` | Create payroll |
| POST | `/api/payroll/update` | Update payroll |
| POST | `/api/payroll/delete` | Delete payroll |
| POST | `/api/payroll/all/fetch` | List payrolls |
| POST | `/api/payroll/export` | Export payroll |
| POST | `/api/payroll/all/export` | Export all payrolls |

### Finance

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/finance/sale/fetch` | Finance sales report |
| POST | `/api/finance/sale/approve` | Approve sale |
| POST | `/api/finance/sale/bill/mark` | Mark bill raised |

### Dashboard

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/dashboard/attendance/today` | Today's attendance |
| POST | `/api/dashboard/attendance/subordinates/today` | Subordinates attendance |
| POST | `/api/dashboard/lead/latest` | Latest leads |
| POST | `/api/dashboard/meeting/latest` | Latest meetings |
| POST | `/api/dashboard/task/latest` | Latest tasks |
| POST | `/api/dashboard/project/latest` | Latest projects |
| POST | `/api/dashboard/demo/latest` | Latest demos |
| POST | `/api/dashboard/visit/latest` | Latest visits |
| POST | `/api/dashboard/sale/latest` | Latest sales |
| POST | `/api/dashboard/subtask/own/fetch` | Own subtasks |
| POST | `/api/dashboard/holidays` | Upcoming holidays |
| POST | `/api/dashboard/birthdays` | Upcoming birthdays |
| POST | `/api/dashboard/call-logs/chart` | Call logs chart data |
| POST | `/api/dashboard/sales/chart` | Sales chart data |
| POST | `/api/dashboard/counts` | Dashboard counts |

### Communication

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/phone/call/logs/uploads` | Upload call data |
| POST | `/api/phone/call/logs/fetch` | Fetch call logs |
| POST | `/api/phone/call/logs/aggregated` | Aggregated call data |
| POST | `/api/phone/call/logs/user/records` | User call records |
| POST | `/api/phone/call/logs/user/records/export` | Export user call records as an Excel workbook |
| POST | `/api/phone/call/logs/number/history` | Number history |
| POST | `/api/ivr/call` | Make IVR call |
| POST | `/api/ivr/call/logs/aggregated` | Aggregated IVR data |
| POST | `/api/ivr/call/logs/fetch` | IVR call logs |
| POST | `/api/ivr/call/logs/fetch/export` | Export IVR call logs as an Excel workbook |
| POST | `/api/ivr/call/logs/number/history` | IVR number history |
| POST | `/api/notification/user/fetch` | User notifications |
| POST | `/api/activity/user/log/fetch` | Activity logs |
| POST | `/api/timesheet/user/fetch` | User timesheet |

### Settings

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/account/update` | Update account |
| POST | `/api/account/logo/update` | Update logo |
| POST | `/api/account/document/create` | Create document |
| POST | `/api/account/document/update` | Update document |
| POST | `/api/account/document/delete` | Delete document |
| POST | `/api/account/document/fetch` | List documents |
| POST | `/api/setting/role/create` | Create role |
| POST | `/api/setting/role/update` | Update role |
| POST | `/api/setting/ivr/credential/update` | Update IVR API key |
| POST | `/api/setting/ivr/credential/fetch` | Fetch IVR API key |
| GET | `/api/meeting/setting/fetch` | List meeting behavior settings (permission: `meeting_management.meeting.read` or `.override`) |
| POST | `/api/meeting/setting/update` | Update meeting behavior settings (permission: `meeting_management.meeting.update` or `.override`) |
| GET | `/api/credit/setting/fetch` | List credit behavior settings (permission: `credit_management.credit_request.read` or `.override`) |
| POST | `/api/credit/setting/update` | Update credit behavior settings (permission: `credit_management.credit_request.update` or `.override`) |
| GET | `/api/notification/setting/fetch` | List notification behavior settings (permission: `notification_management.notification.override`) |
| POST | `/api/notification/setting/update` | Update notification behavior settings (permission: `notification_management.notification.override`) |
| POST | `/api/role/all/fetch` | List roles |
| POST | `/api/role/permission/all/fetch` | List permissions |
| POST | `/api/smtp_credential/fetch` | Fetch SMTP config |
| POST | `/api/smtp_credential/update` | Update SMTP config |
| POST | `/api/sms_credential/fetch` | Fetch SMS config |
| POST | `/api/sms_credential/update` | Update SMS config |
| POST | `/api/waba_credential/fetch` | Fetch WABA config |
| POST | `/api/waba_credential/update` | Update WABA config |
| POST | `/api/rcs_credential/fetch` | Fetch RCS config |
| POST | `/api/rcs_credential/update` | Update RCS config |
| POST | `/api/meta/status` | Meta connection status (`connected`, `status`, `meta_user_id`, `token_expires_at`) |
| POST | `/api/meta/pages` | List connected Meta Pages |
| POST | `/api/meta/leads` | Paginated list of captured `ad_leads` (provider='meta') |
| POST | `/api/meta/connect` | Create/update the Meta connection (`app_id`, `app_secret`, `verify_token`) |
| POST | `/api/meta/disconnect` | Deactivate the Meta connection |
| POST | `/api/meta/forms` | List Lead Ads forms (placeholder — needs real Graph API, returns `[]` until OAuth is wired) |
| POST | `/api/meta/forms/fields` | List a form's fields (placeholder — returns `[]` until OAuth is wired) |
| POST | `/api/meta/forms/mapping` | Save `source_field` -> `crm_field` mappings for a form (writes `ad_field_mappings`) |
| POST | `/api/payment/type/fetch` | Payment types |
| POST | `/api/payment/type/create` | Create payment type |
| POST | `/api/payment/type/update` | Update payment type |
| POST | `/api/payment/type/delete` | Delete payment type |
| POST | `/api/app-version/fetch` | App version info |
| POST | `/api/app-version/create` | Create version |
| POST | `/api/app-version/update` | Update version |
| POST | `/api/app-version/status/update` | Update version status |
| POST | `/api/app-version/delete` | Delete version |
| POST | `/api/app-version/all/fetch` | List versions |

#### Module behavior settings

Behavior settings (meeting overdue buffer, OTP expiry, credit overdue threshold, upcoming-activity
reminder windows, etc.) are admin-configurable through **per-module** endpoints, each backed by its own
settings table (`meeting_settings`, `credit_settings`, `notification_settings`) and gated by that
module's own permission.

- `GET /api/meeting/setting/fetch` → `{success, message, data: {settings: [{key, label, description, type, value}]}}`
- `GET /api/credit/setting/fetch` → same shape for credit settings.
- `GET /api/notification/setting/fetch` → same shape for notification settings.
- `POST /api/meeting/setting/update` (and `credit`, `notification` variants) with body
  `{settings: [{key: "otp_expiry_minutes", value: 10}, ...]}` → returns the updated list.
  Only keys present in that module's catalog (`app/Support/{Module}SettingCatalog.php`) are accepted;
  each `value` is validated against that key's validation rule.

### Linked Devices

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/linked-device/fetch` | List linked devices with device limits |
| POST | `/api/linked-device/aggregated` | **Super admin only.** Aggregated counts (user, total/linked/left devices) and linked devices per user. Optional `user_id` to target another user |
| POST | `/api/linked-device/update-linked-devices` | **Super admin only.** Update linked devices limit (`total_linked_devices_count`). Optional `user_id` to target another user |
| POST | `/api/linked-device/signout` | **Super admin only.** Force sign out for a user (`user_id` required). No `type` revokes the primary `auth_token`; `type=all` deletes all tokens + linked devices; `type=linked_devices` (with `id`) deletes one linked device by id |
| POST | `/api/linked-device/signout-all` | **Super admin only.** Sign out all linked devices for a user (`user_id` required) |
| POST | `/api/linked-device/delete` | Delete one linked device |
| POST | `/api/linked-device/delete-all` | Delete all linked devices |
| POST | `/api/linked-device/qr/confirm` | Confirm QR device link |
| POST | `/api/linked-device/setmpin` | Set device MPIN |
| POST | `/api/linked-device/updatempin` | Update device MPIN |
| POST | `/api/linked-device/resetmpin/send-otp` | Send MPIN reset OTP |
| POST | `/api/linked-device/resetmpin/verify-otp` | Verify OTP and reset MPIN |
| POST | `/api/linked-device/matchmpin` | Verify MPIN |

### App Settings (App MPIN)

6-digit app-level MPIN, separate from the linked-device MPIN above. Stored encrypted (reversible, via
Laravel's `encrypted` cast) in `user_app_settings.app_mpin`, not hashed — allows the reset flow to send the
plaintext MPIN back to the user via notification. Reset uses a 4-digit OTP on `users.otp`/`otp_expires_at`
(10 minute TTL) — the same mechanism `LinkedDevicesController`'s linked-device MPIN reset uses.

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/app-settings/mpin/set` | Set app MPIN (fails if already set) |
| POST | `/api/app-settings/mpin/update` | Update app MPIN (`old_mpin`, `new_mpin`) |
| POST | `/api/app-settings/mpin/reset/send-code` | Send 4-digit OTP via push notification |
| POST | `/api/app-settings/mpin/reset/verify-code` | Verify OTP (`otp`) and issue a new app MPIN |
| POST | `/api/app-settings/mpin/match` | Verify app MPIN (`mpin`) |

MPIN matching (`App\Traits\MatchesAppMpin`) was removed from export endpoints; app MPIN verification is now
only performed via `AppSettingsController@matchMpin` above.

### Marketing — Templates & Campaigns

Multi-channel (SMS/RCS/WhatsApp) template management and campaign sending. Full request/response shapes,
per-channel config requirements, variable-mapping rules, and known provider-side failure modes are
documented separately in **[campaign-management.md](campaign-management.md)** — read that before
implementing this section.

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/sms_template/fetch` \| `/rcs_template/fetch` \| `/waba_template/fetch` | List locally cached templates per channel |
| POST | `/api/sms_template/fetch_from_provider` \| `/rcs_template/...` \| `/waba_template/...` | Pull/refresh one template from the provider by name/ID |
| POST | `/api/sms_template/sync` \| `/rcs_template/...` \| `/waba_template/...` | Bulk upsert a client-supplied template array (no provider call) |
| POST | `/api/sms_template/delete` \| `/rcs_template/...` \| `/waba_template/...` | Delete a locally cached template |
| POST | `/api/campaign/create` | Create a campaign (name/strategy/channels only — no audience) |
| POST | `/api/campaign/update` | Update a `draft` campaign's name/strategy/channels (no audience) |
| POST | `/api/campaign/fetch` | Get one campaign (with `id`) or a paginated list |
| POST | `/api/campaign/delete` | Delete a non-running campaign |
| POST | `/api/campaign/select_lead_group` | Set/clear a campaign's Lead Group audience and import its recipients |
| POST | `/api/campaign/upload_excel` | Upload an Excel/CSV audience file and import its rows as recipients in one call |
| POST | `/api/campaign/select_test_audience` | Set a campaign's audience to a manually-typed list of test phone numbers (replaces any lead-group/Excel audience) |
| POST | `/api/campaign/mapping_fields` | Get available lead/company `column` options for variable mapping (schema-derived) |
| POST | `/api/campaign/map_variables` | Map template `{{n}}` variables to data columns, per channel |
| POST | `/api/campaign/preview` | Render sample output per channel before launch |
| POST | `/api/campaign/launch` | Queue the campaign for sending |
| POST | `/api/campaign/pause` \| `/resume` \| `/stop` | **Currently disabled** — always returns 400, see doc §9 |
| POST | `/api/campaign/fetch_logs` | Per-recipient, per-channel send log |
| POST | `/api/campaign/fetch_stats` | Aggregate sent/failed/delivered counts |
| POST | `/api/campaign/search` | Filter/search campaigns by name/status/strategy/channel/date range |
| POST | `/api/campaign/check_balance` | Fetch the Bluewaves provider account's remaining message balance |

## Common Error Responses

| Status | Meaning |
|--------|---------|
| 401 | Unauthenticated (no token or invalid token) |
| 403 | Forbidden (lacks permission) |
| 422 | Validation error (invalid input) |
| 500 | Server error |

## Pagination

List endpoints accept optional pagination parameters:
- Request body or query params: `page`, `perPage` (or `per_page`)
- Response includes Laravel pagination metadata
- Default perPage varies by endpoint (typically 10-25)

## File Uploads

- Profile images, check-in/out images: multipart POST with `image` field
- Documents, assets: `file` field
- Quotation attachments: `files[]` array (max 5, 5MB each, 20MB total)
- Allowed types: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, JPG, PNG, GIF
- Files stored in `storage/app/private/` or `storage/app/public/`
- Served via web routes at `/storage/{filename}` or `/storage/{category}/{filename}`

## Google Auth Flow

1. Frontend redirects to `GET /api/google/auth`
2. User authorizes on Google
3. Google redirects to `GET /api/google/callback`
4. Tokens stored in database
5. Used for Google Calendar event creation (demos, meetings)
