> ## Documentation Index
> Fetch the complete documentation index at: https://theteam-13fc7f49.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Users Management Endpoints

###### *Users API*

This file documents the endpoints related to user management, user listings, user detail updates, and the utility routes exposed through the user API.

## **Table of Contents**

1. **Overview**
2. **Base URL and Headers**
3. **List Users**
4. **Show User**
5. **Update User**
6. **Archive User**
7. **Unarchive User**
8. **Delete User**
9. **Trainings by Instructor**
10. **Trainings by Student**
11. **Exams by Student**
12. **Get User Cart or Wishlist**
13. **Get User Orders**
14. **List Instructors**
15. **Get Instructor by ID**
16. **Exams by Instructor**
17. **Quizzes by Instructor**
18. **Set Instructor Default Revenue**
19. **Download Order Invoice**
20. **Export Owners CSV**
21. **Owner Emails (JSON)**
22. **Internal Reused Methods**
23. **Known Implementation Notes**

***

## **1. Overview**

This file covers:

* user CRUD-style management routes
* archive / restore / delete flow
* student ownership routes
* instructor listing routes that are still exposed through the user API
* invoice export
* owner CSV export
* owner emails as JSON (by training / exam id or slug)

These routes are inside the authenticated protected route group.

***

## **2. Base URL and Headers**

Base API URL:

```json theme={null}
{{base_url}}/api
```

Suggested Postman variables:

```json theme={null}
{{base_url}} = http://127.0.0.1:8000
{{token}} = your_sanctum_token
```

Headers:

```json theme={null}
Accept: application/json
Content-Type: application/json
Authorization: Bearer {{token}}
```

***

## **3. List Users**

Returns paginated users for one company.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/users` |
| Auth | Bearer Token |

### **Query Parameters**

| **Name** | **Required** | **Notes** |
| :- | :- | :- |
| `company_id` | Yes | must exist in `companies` |
| `role_id` | No | filter by role |
| `search` | No | first name, last name, email |
| `page` | No | default `1` |
| `per_page` | No | default `20`, max `100` |
| `withCompany` | No | include company relation |
| `only_archived` | No | filter archived users |
| `only_deleted` | No | filter soft-deleted users |

### **Example Request**

```json theme={null}
GET /api/users?company_id=1&role_id=2&search=jane&page=1&per_page=10&withCompany=true
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "current_page": 1,
  "data": [
    {
      "id": 7,
      "company_id": 1,
      "role_id": 2,
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "jane.doe@example.com",
      "phone_number": "+21611111111",
      "birthday": "1994-06-20",
      "bio": "Senior instructor",
      "blocked_at": null,
      "archived_at": null,
      "deleted_at": null,
      "fullname": "Jane Doe",
      "role": {
        "id": 2,
        "label": "Instructor",
        "permissions": []
      },
      "image": null,
      "lessons": [],
      "trainings_as_instructor": [],
      "company": {
        "id": 1,
        "label": "Elearny"
      }
    }
  ],
  "per_page": 10,
  "total": 1,
  "last_page": 1
}
```

***

## **4. Show User**

Returns one user by ID.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/user/{id}` |
| Auth | Bearer Token |

### **Example Request**

```json theme={null}
GET /api/user/7
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "id": 7,
  "company_id": 1,
  "role_id": 2,
  "first_name": "Jane",
  "last_name": "Doe",
  "email": "jane.doe@example.com",
  "phone_number": "+21611111111",
  "birthday": "1994-06-20",
  "bio": "Senior instructor",
  "blocked_at": null,
  "default_revenue_percentage": 25,
  "public_profile_skills": "AI, Product, Prompt Engineering",
  "public_profile_linkedin_url": "https://linkedin.com/in/janedoe",
  "public_profile_website": "https://janedoe.dev",
  "public_profile_description": "Instructor and consultant",
  "public_profile_about_me": "I teach product and AI topics.",
  "public_profile_phone_number": "+21611111111",
  "public_profile_location": "Tunis, Tunisia",
  "public_profile_email": "jane.doe@example.com",
  "public_profile_verification": true,
  "hide_public_profile": false,
  "role": {
    "id": 2,
    "label": "Instructor",
    "permissions": []
  },
  "image": null,
  "lessons": [],
  "trainings_as_instructor": [],
  "fullname": "Jane Doe"
}
```

Note:

* if `hide_public_profile = true`, the controller removes public profile fields from the response

***

## **5. Update User**

Updates user core fields, blocking state, password, and instructor public profile data.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/user/{id}` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "role_id": 2,
  "first_name": "Jane",
  "last_name": "Doe",
  "phone_number": "+21611111111",
  "image_id": 12,
  "birthday": "1994-06-20",
  "bio": "Senior instructor",
  "is_blocked": false,
  "password": "updatedSecret123",
  "password_confirmation": "updatedSecret123",
  "public_profile_skills": "AI, Product, Prompt Engineering",
  "public_profile_linkedin_url": "https://linkedin.com/in/janedoe",
  "public_profile_website": "https://janedoe.dev",
  "public_profile_description": "Instructor and consultant",
  "public_profile_about_me": "I teach product and AI topics.",
  "public_profile_phone_number": "+21611111111",
  "public_profile_location": "Tunis, Tunisia",
  "public_profile_email": "jane.doe@example.com",
  "public_profile_verification": true,
  "hide_public_profile": false
}
```

### **Success Response**

```json theme={null}
{
  "message": "User updated successfully."
}
```

### **Conflict Example**

```json theme={null}
{
  "message": "This instructor already has trainings attached to them, you cannot change their role."
}
```

Important notes:

* setting `role_id = 2` makes the public profile visible
* setting another role hides the public profile
* `public_profile_verification` is only applied when the authenticated user is admin or super-admin

***

## **6. Archive User**

Archives a user by filling `archived_at`.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/user/{id}/archive` |
| Auth | Bearer Token |

### **Success Response**

```json theme={null}
{
  "message": "User archived successfully."
}
```

***

## **7. Unarchive User**

Restores an archived user by clearing `archived_at`.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/user/{id}/unarchive` |
| Auth | Bearer Token |

### **Success Response**

```json theme={null}
{
  "message": "User restored successfully.",
  "user": {
    "id": 7,
    "company_id": 1,
    "role_id": 2,
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane.doe@example.com",
    "archived_at": null
  }
}
```

***

## **8. Delete User**

Deletes an already archived user.

| **Field** | **Value** |
| :- | :- |
| Method | `DELETE` |
| URL | `/api/user/{id}/delete` |
| Auth | Bearer Token |

### **Success Response**

```json theme={null}
{
  "message": "User force deleted successfully."
}
```

Important note:

* despite the message text, this is a soft delete through Laravel `deleted_at`

***

## **9. Trainings by Instructor**

Returns paginated trainings belonging to one instructor.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/user/{instructor_id}/trainings_by_instructor` |
| Auth | Bearer Token |

### **Example Request**

```json theme={null}
GET /api/user/7/trainings_by_instructor?search=ai&page=1&per_page=10
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "current_page": 1,
  "data": [
    {
      "id": 101,
      "label": "AI for Product Managers",
      "instructor_id": 7,
      "categories": [],
      "tags": [],
      "instructor": {
        "id": 7,
        "first_name": "Jane",
        "last_name": "Doe"
      },
      "chapters": [],
      "reviews": [],
      "image": null,
      "video": null
    }
  ],
  "per_page": 10,
  "total": 1
}
```

***

## **10. Trainings by Student**

Returns the paid public trainings owned by a student.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/user/{id}/trainings_by_student` |
| Auth | Bearer Token |

### **Example Request**

```json theme={null}
GET /api/user/15/trainings_by_student
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
[
  {
    "id": 101,
    "label": "AI for Product Managers",
    "slug": "ai-for-product-managers",
    "is_public": true,
    "image": null,
    "video": null,
    "instructor": {
      "id": 7,
      "first_name": "Jane",
      "last_name": "Doe"
    },
    "chapters": [],
    "quiz": []
  }
]
```

***

## **11. Exams by Student**

Returns the standalone exams owned by a student through paid orders.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/user/{id}/exams_by_student` |
| Auth | Bearer Token |

### **Example Request**

```json theme={null}
GET /api/user/15/exams_by_student
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
[
  {
    "id": 88,
    "label": "AI Fundamentals Final Exam",
    "slug": "ai-fundamentals-final-exam",
    "training_id": null,
    "chapter_id": null,
    "image": null,
    "quiz_questions": []
  }
]
```

***

## **12. Get User Cart or Wishlist**

Returns the latest order shell for the given type.

Order type values used by the project:

* `1` = wishlist
* `2` = cart
* `3` = order

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/user/{student_id}/cart/{type}` |
| Auth | Bearer Token |

### **Example Request**

```json theme={null}
GET /api/user/15/cart/2
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "id": 302,
  "student_id": 15,
  "type": 2,
  "order_status_id": 2,
  "coupon_id": 5,
  "coupon_code": "WELCOME20",
  "trainings": [],
  "quizzes": [],
  "coupon": {
    "id": 5,
    "code": "WELCOME20"
  }
}
```

***

## **13. Get User Orders**

Returns the user's orders where `type = 3`.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/user/{student_id}/orders` |
| Auth | Bearer Token |

### **Example Request**

```json theme={null}
GET /api/user/15/orders
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
[
  {
    "id": 401,
    "student_id": 15,
    "type": 3,
    "order_status_id": 5,
    "reference": "REF-ABCD123456",
    "trainings": [],
    "quizzes": [],
    "order_status": {
      "id": 5,
      "label": "Paid Online"
    }
  }
]
```

***

## **14. List Instructors**

Returns paginated instructors for one company.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/instructors` |
| Auth | Bearer Token |

### **Query Parameters**

| **Name** | **Required in practice** | **Notes** |
| :- | :- | :- |
| `company_id` | Yes | used by the controller filter |
| `search` | No | first name / last name |
| `page` | No | default `1` |
| `per_page` | No | default `15` |

### **Example Request**

```json theme={null}
GET /api/instructors?company_id=1&search=jane&page=1&per_page=10
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "current_page": 1,
  "data": [
    {
      "id": 7,
      "company_id": 1,
      "role_id": 2,
      "first_name": "Jane",
      "last_name": "Doe",
      "image": null,
      "trainings_as_instructor": [],
      "exams_as_instructor": []
    }
  ],
  "per_page": 10,
  "total": 1,
  "catalog_counts": { "trainings": 27, "exams": 6, "instructors": 4 }
}
```

**`catalog_counts` (page 1 only, when `company_id` is sent).** The size of each student Marketplace catalog of the company: `trainings` (public trainings), `exams` (published standalone exams) and `instructors` (users with role `2`). They are counted without the request's own filters (search, categories, price, …), leave archived and deleted rows out, and are cached per company for 5 minutes; the cache is cleared as soon as a training, exam or instructor is created, published / unpublished, archived, deleted or restored (`app/Services/MarketplaceCountsService.php`, `MarketplaceCountsObserver`). The same object comes on page 1 of `GET /preview-trainings`, `GET /preview-exams` and `GET /instructors`, so the app fills the three Marketplace tab counts from whichever catalog loads first, with no extra request. Pages 2 and later do not carry it.

***

## **15. Get Instructor by ID**

Returns one instructor from the authenticated user's company.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/instructor/{id}` |
| Auth | Bearer Token |

### **Example Request**

```json theme={null}
GET /api/instructor/7
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "id": 7,
  "company_id": 1,
  "role_id": 2,
  "first_name": "Jane",
  "last_name": "Doe",
  "image": null,
  "background_items": []
}
```

***

## **16. Exams by Instructor**

Returns the standalone exams created by the instructor.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/instructors/{instructor_id}/exams` |
| Auth | Bearer Token |

### **Example Request**

```json theme={null}
GET /api/instructors/7/exams
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
[
  {
    "id": 88,
    "label": "AI Fundamentals Final Exam",
    "training_id": null,
    "chapter_id": null,
    "categories": [],
    "reviews": [],
    "image": null,
    "orders": [],
    "quiz_questions": []
  }
]
```

***

## **17. Quizzes by Instructor**

This route exists in `routes/api.php`:

```json theme={null}
GET /api/instructors/{instructor_id}/quizzes
```

But there is currently no matching `quizzes_by_instructor()` method in the controller class.

### **Example Request**

```json theme={null}
GET /api/instructors/7/quizzes
Authorization: Bearer {{token}}
Accept: application/json
```

### **Current Practical Result**

In local/debug mode, a typical server error is:

```json theme={null}
{
  "message": "Method App\\Http\\Controllers\\UsersController::quizzes_by_instructor does not exist."
}
```

Important note:

* this route is declared, but it is not currently implemented

***

## **18. Set Instructor Default Revenue**

Updates `default_revenue_percentage` for an instructor.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/user/{instructor_id}/set-instructor-default-revenue` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "default_revenue_percentage": 30
}
```

### **Success Response**

```json theme={null}
{
  "message": "Default revenue percentage updated successfully.",
  "default_revenue_percentage": 30
}
```

### **Error Example**

```json theme={null}
{
  "message": "The given user is not an instructor."
}
```

***

## **19. Download Order Invoice**

Downloads the authenticated user's invoice PDF for a paid order.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/user/orders_invoice/{order_id}` |
| Auth | Bearer Token |

### **Example Request**

```json theme={null}
GET /api/user/orders_invoice/401
Authorization: Bearer {{token}}
Accept: application/pdf
```

### **Success Behavior**

* returns a PDF stream
* content type is `application/pdf`

### **Error Example for Unpaid Order**

```json theme={null}
{
  "message": "Invoice is only available for paid orders. Current order status: Pending"
}
```

***

## **20. Export Owners CSV**

Exports user ownership for one training or one exam.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/users/export-owners` |
| Auth | Bearer Token |

### **Request JSON for Training**

```json theme={null}
{
  "training_id": 101
}
```

### **Request JSON for Exam**

```json theme={null}
{
  "exam_id": 88
}
```

### **Validation Error Example**

```json theme={null}
{
  "message": "Provide exactly one of training_id or exam_id"
}
```

### **Success Behavior**

* successful response is a CSV file download
* need the same list as JSON, or by slug instead of id? See [21. Owner Emails (JSON)](#21-owner-emails-json) — both endpoints share the same ownership query in `UsersController::ownersQuery()`
* CSV columns are:

```json theme={null}
user_id,user_mail
15,student1@example.com
22,student2@example.com
```

***

## **21. Owner Emails (JSON)**

Returns, as JSON, the emails of every user who **owns** one training or one standalone exam — the same ownership rule as the CSV export above (order `type = 3`, status **Paid Online** `5` or **Paid Offline** `4`, order item `access_revoked = false`), restricted to your company. Each user is listed once even with several paid orders, archived and soft-deleted users are skipped, and the list is ordered by user id. Made for the "who bought this?" question: copy `emails_comma_separated` straight into the recipients field of a mail client.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/users/owners-emails` |
| Auth | Bearer Token — `viewAny` on `User` (super-admin, admin, or permission `users-viewAny` = `51`) |

### **Query Parameters**

Exactly **one** of the four selectors is required.

| **Name** | **Required** | **Notes** |
| :- | :- | :- |
| `training_id` | one of the four | id of a training of your company |
| `training_slug` | one of the four | slug of a training of your company (case-insensitive) |
| `exam_id` | one of the four | id of a **standalone** exam (`training_id` and `chapter_id` both `null`) of your company |
| `exam_slug` | one of the four | slug of a standalone exam (case-insensitive) |
| `company_id` | No | **Super-admins only**: the company to search (required for a super-admin without a company). Everyone else is locked to their own company; another value answers `403`. |

### **Example Requests**

```json theme={null}
GET /api/users/owners-emails?training_id=101
GET /api/users/owners-emails?training_slug=intro-to-pmp
GET /api/users/owners-emails?exam_id=88
GET /api/users/owners-emails?exam_slug=advanced-python-exam
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response — training**

```json theme={null}
{
  "type": "training",
  "id": 101,
  "slug": "intro-to-pmp",
  "label": "Intro to PMP",
  "company_id": 1,
  "total": 2,
  "emails": [
    "sarra@example.com",
    "karim@example.com"
  ],
  "emails_comma_separated": "sarra@example.com, karim@example.com"
}
```

### **Success Response — exam**

```json theme={null}
{
  "type": "exam",
  "id": 88,
  "slug": "advanced-python-exam",
  "label": "Advanced Python Exam",
  "company_id": 1,
  "total": 1,
  "emails": [
    "sarra@example.com"
  ],
  "emails_comma_separated": "sarra@example.com"
}
```

* `label` is the training / exam title (there is no separate `title` column).
* `emails_comma_separated` joins the addresses with a comma and a space (`, `); `total` is the number of distinct emails.
* No role filter is applied: like the CSV export, any user with a paid, non-revoked order is listed (students assigned for free through `POST /order/assign_trainings_to_students` / `POST /order/assign_exams` count as Paid Offline owners).

### **Error Responses**

```json theme={null}
422 { "message": "Provide exactly one of training_id, training_slug, exam_id or exam_slug" }
422 { "message": "The training id must be an integer.", "errors": { "training_id": ["The training id must be an integer."] } }
422 { "message": "Company ID is required but not found. Pass company_id." }   ← super-admin without a company and without company_id
404 { "message": "Training not found." }
404 { "message": "Exam not found. Only standalone exams (no training_id and no chapter_id) can be sold." }
403 { "message": "Unauthorized to fetch data of other companies." }         ← company_id of another company (non super-admin)
403 { "message": "This action is unauthorized." }                           ← caller without users-viewAny (e.g. a student)
401 { "message": "Unauthorized" }
```

> `404` also covers archived and soft-deleted trainings / exams (they are hidden by the model scopes), and a quiz that belongs to a training or chapter (not sellable, so it has no owners).

***

## **22. Internal Reused Methods**

These methods are part of the same controller flow but do not have their own route:

* `buildInvoicePdfViewData(Order $order)` Brief role: builds the final invoice payload used by `downloadOrderInvoice()`.
* `getThemeForCompany(int $companyId)` Brief role: loads company name and platform logo used by invoice generation.

***

## **23. Known Implementation Notes**

* `DELETE /api/user/{id}/delete` performs a soft delete even though the response message says `force deleted`.
* `archive` uses `archived_at`, while delete uses Laravel `deleted_at`.
* `show()` removes public profile fields when `hide_public_profile = true`.
* update uses `POST /api/user/{id}`, not `PUT` or `PATCH`.
* `/api/instructors/{instructor_id}/quizzes` is declared but not implemented.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.