# CEPI LMS — API Contracts

> Covers existing web JSON endpoints and the complete planned REST API required for Flutter mobile app.
> Last updated: 2026-06-02.

---

## 1. Existing JSON Endpoints (Web, No Sanctum)

These are the only JSON-returning endpoints in the current web app:

### POST /admin/videos/upload
- Auth: `auth + role:admin`
- Purpose: Creates a video placeholder on Bunny Stream
- Request: `{ title: string }`
- Response: `{ status: 'success', video_id: 'uuid', library_id: 'string' }`
- Error: `{ status: 'error', message: 'string' }` (500)

### POST/PUT/DELETE /admin/modules, /admin/lessons
- Auth: `auth + role:admin`
- Purpose: Module/Lesson CRUD via AJAX
- Standard Laravel `apiResource` JSON responses

### POST /learn/lessons/{lesson}/progress
- Auth: `auth` + active enrollment verified in controller
- Request body:
  ```json
  {
    "is_completed": true,
    "watched_seconds": 120,
    "last_position_seconds": 118,
    "duration_seconds": 600,
    "started": false
  }
  ```
- Response: `{ "status": "success", "is_completed": true }`

---

## 2. Standard API Response Format (Mobile API — To Be Built)

All mobile API endpoints must follow this envelope:

### Success
```json
{
  "success": true,
  "data": { ... },
  "message": null,
  "meta": {
    "pagination": { ... }
  }
}
```

### Error
```json
{
  "success": false,
  "data": null,
  "message": "Human-readable error description",
  "errors": {
    "field": ["Validation error message"]
  },
  "code": "ERROR_CODE"
}
```

### Pagination (when applicable)
```json
"meta": {
  "pagination": {
    "current_page": 1,
    "last_page": 5,
    "per_page": 15,
    "total": 72,
    "from": 1,
    "to": 15
  }
}
```

### HTTP Status Codes
| Scenario | Code |
|---|---|
| Success (data returned) | 200 |
| Created | 201 |
| No content | 204 |
| Validation error | 422 |
| Unauthenticated | 401 |
| Unauthorized / Forbidden | 403 |
| Not found | 404 |
| Server error | 500 |

---

## 3. API Versioning

All mobile API routes prefixed: `/api/v1/`

Install and configure: `composer require laravel/sanctum`

Auth guard: `auth:sanctum`

---

## 4. Authentication API

### POST /api/v1/auth/register
- Auth: None
- Request:
  ```json
  {
    "name": "string|required|max:255",
    "email": "string|required|email|unique:users",
    "password": "string|required|min:8|confirmed",
    "password_confirmation": "string|required",
    "phone": "string|nullable|max:30",
    "gender": "string|nullable|in:male,female,other"
  }
  ```
- Response (201):
  ```json
  {
    "success": true,
    "data": {
      "user": { "id": 1, "name": "...", "email": "..." },
      "token": "sanctum_token_string",
      "requires_email_verification": true
    }
  }
  ```

### POST /api/v1/auth/login
- Auth: None
- Request:
  ```json
  {
    "email": "string|required|email",
    "password": "string|required"
  }
  ```
- Response (200):
  ```json
  {
    "success": true,
    "data": {
      "user": { "id": 1, "name": "...", "email": "...", "user_type": "student", "avatar": null },
      "token": "sanctum_token_string"
    }
  }
  ```
- Errors: 401 invalid credentials, 403 account inactive
- Note: Device binding logic differs for mobile (cookie-based binding doesn't apply to mobile tokens; use separate `device_id` tracking)

### POST /api/v1/auth/verify-email
- Auth: Bearer token
- Request: `{ "code": "6-digit string" }`
- Response (200): `{ "success": true, "data": { "verified": true } }`
- Error: 422 if code invalid/expired

### POST /api/v1/auth/resend-verification
- Auth: Bearer token
- Response (200): `{ "success": true, "message": "Verification code sent" }`
- Rate limit: 6 per minute

### POST /api/v1/auth/forgot-password
- Auth: None
- Request: `{ "email": "string|required|email" }`
- Response (200): `{ "success": true, "message": "Reset link sent if email exists" }`

### POST /api/v1/auth/logout
- Auth: Bearer token
- Action: Deletes current Sanctum token
- Response (200): `{ "success": true, "message": "Logged out" }`

### GET /api/v1/auth/me
- Auth: Bearer token
- Response (200):
  ```json
  {
    "success": true,
    "data": {
      "id": 1,
      "name": "...",
      "email": "...",
      "user_type": "student",
      "avatar": "url",
      "phone": "...",
      "email_verified_at": "2026-04-01T10:00:00Z"
    }
  }
  ```

---

## 5. Course API

### GET /api/v1/courses
- Auth: None (public)
- Query params: `page`, `per_page`, `category`, `level`, `search`, `sort` (`newest`/`popular`/`price_asc`/`price_desc`)
- Response (200):
  ```json
  {
    "success": true,
    "data": [
      {
        "id": "uuid",
        "title": "...",
        "slug": "...",
        "thumbnail": "url",
        "price": "3500.00",
        "discount_price": null,
        "currency": "PKR",
        "is_free": false,
        "level": "beginner",
        "rating": "4.50",
        "total_students": 120,
        "total_lectures": 45,
        "total_duration": 18000,
        "instructor": { "id": 2, "name": "...", "avatar": "url" },
        "category": { "id": 1, "name": "FSc Physics" }
      }
    ],
    "meta": { "pagination": { ... } }
  }
  ```

### GET /api/v1/courses/{slug}
- Auth: Optional (affects `is_enrolled` field)
- Response (200):
  ```json
  {
    "success": true,
    "data": {
      "id": "uuid",
      "title": "...",
      "slug": "...",
      "description": "...",
      "thumbnail": "url",
      "promo_video_url": "...",
      "price": "3500.00",
      "discount_price": null,
      "is_free": false,
      "level": "beginner",
      "language": "english",
      "rating": "4.50",
      "total_students": 120,
      "total_lectures": 45,
      "total_duration": 18000,
      "duration_weeks": 8,
      "instructor": { "id": 2, "name": "...", "avatar": "url", "bio": "..." },
      "category": { "id": 1, "name": "FSc Physics" },
      "is_enrolled": true,
      "modules": [
        {
          "id": "uuid",
          "title": "...",
          "order_index": 0,
          "lessons": [
            {
              "id": "uuid",
              "title": "...",
              "content_type": "video",
              "duration_seconds": 900,
              "order_index": 0,
              "is_preview": false
            }
          ]
        }
      ]
    }
  }
  ```

---

## 6. Enrollment API

### GET /api/v1/enrollments
- Auth: Bearer token
- Returns active enrollments for authenticated student
- Response (200):
  ```json
  {
    "success": true,
    "data": [
      {
        "id": "uuid",
        "course": { "id": "uuid", "title": "...", "thumbnail": "url", "slug": "..." },
        "status": "active",
        "progress_percent": "35.50",
        "enrolled_at": "2026-04-10T08:00:00Z",
        "expires_at": null
      }
    ]
  }
  ```

### POST /api/v1/enrollments/free
- Auth: Bearer token
- Request: `{ "course_id": "uuid" }`
- Action: Enrolls student in free course
- Response (201): `{ "success": true, "data": { "enrollment": { ... } } }`
- Errors: 422 if course is not free, 409 if already enrolled

---

## 7. Lesson API

### GET /api/v1/courses/{slug}/lessons/{lesson_id}
- Auth: Bearer token + active enrollment verified
- Response (200):
  ```json
  {
    "success": true,
    "data": {
      "id": "uuid",
      "title": "...",
      "description": "...",
      "content_type": "video",
      "duration_seconds": 900,
      "is_preview": false,
      "bunny_embed_url": "https://player.mediadelivery.net/embed/...",
      "article_content": null,
      "progress": {
        "watched_seconds": 450,
        "last_position_seconds": 450,
        "completed": false,
        "watch_count": 2
      },
      "module": { "id": "uuid", "title": "...", "order_index": 0 }
    }
  }
  ```
- Note: `bunny_embed_url` is a server-signed URL with short TTL (300s). Client must request a fresh URL on each session, not cache it.
- Errors: 403 if not enrolled, 404 if lesson not in this course

### POST /api/v1/lessons/{lesson_id}/progress
- Auth: Bearer token + active enrollment
- Request:
  ```json
  {
    "watched_seconds": 500,
    "last_position_seconds": 495,
    "duration_seconds": 900,
    "is_completed": false,
    "started": true
  }
  ```
- Response (200): `{ "success": true, "data": { "is_completed": false, "progress_percent": "42.00" } }`

---

## 8. Payment API

### GET /api/v1/payments
- Auth: Bearer token
- Returns student's payment history
- Response (200):
  ```json
  {
    "success": true,
    "data": [
      {
        "id": 1,
        "course": { "id": "uuid", "title": "...", "slug": "..." },
        "gateway": "bank_transfer",
        "amount": "3500.00",
        "currency": "PKR",
        "status": "pending",
        "submitted_at": "2026-05-10T09:00:00Z",
        "paid_at": null
      }
    ]
  }
  ```

### POST /api/v1/payments/bank-transfer
- Auth: Bearer token
- Request:
  ```json
  {
    "course_id": "uuid",
    "payer_name": "string|required",
    "payer_phone": "string|required|max:30",
    "transfer_reference": "string|required",
    "transfer_notes": "string|nullable|max:2000"
  }
  ```
- Response (201): `{ "success": true, "data": { "payment": { "id": 1, "status": "pending", ... } } }`
- Errors: 409 if already enrolled, 422 validation errors

### GET /api/v1/payments/bank-details
- Auth: Bearer token
- Response (200):
  ```json
  {
    "success": true,
    "data": {
      "bank_name": "HBL",
      "account_title": "CEPI Pakistan",
      "account_number": "28679014582103",
      "whatsapp": "0321 111 4880"
    }
  }
  ```

---

## 9. Profile API

### PUT /api/v1/profile
- Auth: Bearer token
- Request:
  ```json
  {
    "name": "string|required",
    "phone": "string|nullable",
    "gender": "string|nullable|in:male,female,other",
    "bio": "string|nullable|max:1000"
  }
  ```
- Response (200): `{ "success": true, "data": { "user": { ... } } }`

### POST /api/v1/profile/avatar
- Auth: Bearer token
- Request: `multipart/form-data` with `avatar` image
- Response (200): `{ "success": true, "data": { "avatar_url": "..." } }`

### PUT /api/v1/profile/password
- Auth: Bearer token
- Request: `{ "current_password": "...", "password": "...", "password_confirmation": "..." }`
- Response (200): `{ "success": true, "message": "Password updated" }`

---

## 10. Dashboard API

### GET /api/v1/dashboard
- Auth: Bearer token (student)
- Response (200):
  ```json
  {
    "success": true,
    "data": {
      "stats": {
        "active_courses": 3,
        "completed_lessons": 22,
        "completion_rate": 48,
        "learning_hours": 12.5
      },
      "enrollments": [ { ... } ],
      "recent_progress": [ { ... } ],
      "pending_payments": [ { ... } ]
    }
  }
  ```

---

## 11. Error Codes Reference

| Code | Meaning |
|---|---|
| `INVALID_CREDENTIALS` | Wrong email/password |
| `EMAIL_NOT_VERIFIED` | Account not verified |
| `ACCOUNT_INACTIVE` | Account suspended |
| `NOT_ENROLLED` | No active enrollment for course |
| `ENROLLMENT_EXPIRED` | Enrollment exists but expired |
| `ALREADY_ENROLLED` | Duplicate enrollment attempt |
| `COURSE_NOT_PUBLISHED` | Course not available |
| `LESSON_NOT_IN_COURSE` | Lesson-course mismatch |
| `PAYMENT_ALREADY_PENDING` | Duplicate bank transfer |
| `DEVICE_BLOCKED` | Mobile token invalidated |
| `TOKEN_EXPIRED` | Sanctum token expired |
| `VALIDATION_ERROR` | Request body validation failed |

---

## 12. API Security Notes

- All write endpoints require `auth:sanctum` middleware
- Rate limiting: `throttle:60,1` on all API routes; `throttle:5,1` on login/register
- No raw Bunny credentials ever returned in API responses
- `bunny_embed_url` is always server-generated with short TTL (300s)
- Pagination enforced on all list endpoints (max 50 per page)
- User can only access their own enrollments, progress, and payments
- Admin API (if added) must use separate `admin:sanctum` guard or ability check
