# MeeChat Mobile API Documentation

**Base URL:** `/api/mobile`  
**Auth:** `Bearer <token>` via Laravel Sanctum  
**Content-Type:** `application/json`

---

## Authentication

### POST `/auth/login`
Login with email/password, returns Sanctum token.

| Field | Type | Required |
|-------|------|----------|
| email | string | ✅ |
| password | string | ✅ |
| device_name | string | ✅ |

**Response:** `{ success, token, user, role }`

### POST `/auth/logout`
Revoke current token.

### POST `/auth/refresh`
Refresh token expiry.

### GET `/auth/me`
Get authenticated user profile.

### POST `/auth/device/register`
Register push notification device.

| Field | Type | Required |
|-------|------|----------|
| device_id | string | ✅ |
| fcm_token | string | ✅ |
| platform | string | ✅ (`android`/`ios`) |

### DELETE `/auth/device/{device_id}`
Unregister a device.

---

## Dashboard

### GET `/dashboard`
Get dashboard stats (total chats, unread, agents online, etc).

---

## Conversations

### GET `/conversations`
List conversations with cursor-based lazy loading.

| Query Param | Type | Default | Description |
|------------|------|---------|-------------|
| status | string | all | `open`, `closed` |
| unread_only | bool | false | Filter unread only |
| assigned_to_me | bool | false | Agent: show only my chats |
| only_contacts | bool | false | Show only contacts |
| waba_pool_id | int | - | Filter by specific WABA |
| search | string | - | Search name/number/content |
| limit | int | 50 | Max 200 |

**Response:**
```json
{
  "success": true,
  "data": [{
    "id": 1,
    "customerNumber": "628xxx",
    "customerName": "John",
    "unreadCount": 3,
    "status": "open",
    "agent": { "id": 1, "name": "Agent A" },
    "waba": { "id": 1, "phoneNumber": "628xxx", "name": "WABA 1" },
    "lastMessage": {
      "preview": "Hello...",
      "type": "text",
      "timestamp": "2026-02-18T12:00:00+07:00"
    },
    "lastMessageAt": "2026-02-18T12:00:00+07:00"
  }],
  "meta": { "limit": 50, "count": 10 }
}
```

### GET `/conversations/counts`
Get conversation count statistics.

**Response:**
```json
{
  "success": true,
  "data": { "all": 150, "open": 120, "closed": 30, "unread": 15 }
}
```

### GET `/conversations/{id}`
Get single conversation detail (includes `notes`, `assigned_at`, `is_locked`).

### POST `/conversations/{id}/claim`
Agent claims an unassigned conversation.

### POST `/conversations/{id}/release`
Agent releases a conversation.

### POST `/conversations/{id}/close`
Close a conversation.

### POST `/conversations/{id}/reopen`
Reopen a closed conversation.

### PUT `/conversations/{id}/notes`
Save notes for a conversation.

| Field | Type | Required | Max |
|-------|------|----------|-----|
| notes | string\|null | ❌ | 2000 chars |

**Response:**
```json
{ "success": true, "message": "Notes saved successfully", "data": { "notes": "..." } }
```

### PUT `/conversations/{id}/contact`
Save/update contact information.

| Field | Type | Required | Max |
|-------|------|----------|-----|
| name | string | ✅ | 255 |
| email | string\|null | ❌ | 255 |
| tags | string[] | ❌ | 50 each |

**Response:**
```json
{
  "success": true,
  "data": { "contact_id": 1, "name": "John", "email": "j@x.com", "tags": ["vip"], "phone_number": "628xxx" }
}
```

---

## Messages

### GET `/conversations/{id}/messages`
List messages with cursor-based bidirectional lazy loading.

| Query Param | Type | Default | Description |
|------------|------|---------|-------------|
| cursor | string | - | Base64 cursor for pagination |
| limit | int | 30 | Max 100 |
| direction | string | `before` | `before` (older) / `after` (newer) |

**Response:**
```json
{
  "success": true,
  "data": {
    "messages": [{
      "id": 1,
      "wa_message_id": "wamid.xxx",
      "type": "text",
      "body": "Hello!",
      "direction": "incoming",
      "status": null,
      "timestamp": "2026-02-18T12:00:00",
      "sent_by": null,
      "media_url": null,
      "media_filename": null,
      "media_mime_type": null
    }],
    "meta": {
      "has_more": true,
      "next_cursor": "eyJ...",
      "prev_cursor": "eyJ..."
    }
  }
}
```

### GET `/conversations/{id}/messages/search`
Search messages in a conversation by content.

| Query Param | Type | Default | Description |
|------------|------|---------|-------------|
| q | string | - | **Required.** Search query |
| page | int | 1 | Page number |
| per_page | int | 50 | Max 200 |

**Response:**
```json
{
  "success": true,
  "data": [{
    "id": "out_123",
    "direction": "outgoing",
    "type": "text",
    "body": "matching text...",
    "timestamp": "2026-02-18T12:00:00+07:00",
    "wa_message_id": "wamid.xxx"
  }],
  "meta": { "total": 5, "current_page": 1, "per_page": 50 }
}
```

### POST `/conversations/{id}/send`
Send a message (synchronous, no queue).

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| type | string | ✅ | `text`, `image`, `video`, `audio`, `document` |
| content | string | for text | Message body |
| media_url | string | for media | Media URL |
| caption | string | ❌ | Caption for image/video/document |
| filename | string | ❌ | Filename for document |

**Judol Filter:** Text messages are checked. If blocked:
```json
{
  "success": false,
  "error_code": "CONTENT_BLOCKED",
  "judol_blocked": true,
  "violation_count": 3,
  "violation_limit": 30
}
```

### POST `/conversations/{id}/send-template`
Send a template message (queued).

| Field | Type | Required |
|-------|------|----------|
| template_id | int | ✅* |
| template_name | string | ✅* |
| language | string | ❌ (default: `id`) |
| components | array | ❌ |

*One of `template_id` or `template_name` required.

### POST `/conversations/{id}/mark-read`
Mark messages as read.

| Field | Type | Required |
|-------|------|----------|
| message_ids | int[] | ❌ (empty = mark all) |

### DELETE `/conversations/{id}/messages/{messageId}`
Delete a message (soft delete).

- `messageId`: numeric = incoming, `out_123` = outgoing
- Outgoing within 1h: attempts WhatsApp revoke via Graph API

**Response:**
```json
{ "success": true, "revoked_via_wa": true, "message": "Message deleted for everyone." }
```

### PUT `/conversations/{id}/messages/{messageId}`
Edit outgoing text message. `messageId` must be `out_123` format.

| Field | Type | Required |
|-------|------|----------|
| message | string | ✅ (max 4096) |

Marks original as edited, sends correction: `✏️ Perbaikan pesan sebelumnya:\n{content}`

---

## Templates

### GET `/templates`
List available templates.

### GET `/templates/{id}`
Get template detail with components.

---

## Media

### POST `/media/upload`
Upload media file. Returns URL for use in send.

### GET `/media/limits`
Get upload size/type limits.

---

## Broadcasting

### POST `/broadcasting/auth`
Pusher auth endpoint for real-time events.

---

## Error Codes

| Code | HTTP | Description |
|------|------|-------------|
| `CONVERSATION_NOT_FOUND` | 404 | Conversation doesn't exist |
| `WABA_NOT_FOUND` | 500 | WABA not configured |
| `WABA_TOKEN_MISSING` | 500 | WABA token expired |
| `SERVICE_WINDOW_EXPIRED` | 422 | 24h window closed, use template |
| `CONTENT_BLOCKED` | 422 | Judol filter blocked message |
| `WHATSAPP_API_ERROR` | 400 | Meta API returned error |
| `FORBIDDEN` | 403 | No permission |
| `ALREADY_ASSIGNED` | 400 | Chat already claimed |
| `INVALID_STATE` | 400 | Wrong conversation state |
| `NOT_ALLOWED` | 403 | Agent lacks permission |
| `TEMPLATE_NOT_FOUND` | 404 | Template doesn't exist |
