> ## 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.

# Certifications Endpoints (v2) 

## **Table of Contents**

* **Overview**
* **Authentication**
* **Base URL**
* **Endpoints**
  * **1. Get Certified Trainings by Student**
  * **2. Get Certified Exams by Student**
  * **3. Get Certified Students by Training**
  * **4. Get Certified Students by Exam**
  * **5. Get Certifications by User**
  * **6. Create Certification**
  * **7. Get Certification Details**
  * **8. Get Global Certification Config**
  * **9. Set Global Certification Config**
  * **10. Generate Certification Handler URL**
  * **11. Verify Certification**
  * **12. View Certificate Handler (Web Page)**
  * **12b. Certificate Preview Image (PNG)**
  * **13. View Certification Verification Page**
  * **14. Render Certificate from JSON**
* **Postman Setup**
* **Certificate Payload Reference**
* **Error Handling**

***

## **Overview**

The Certifications API allows you to create, view, verify, and configure training and exam completion certificates.

The current flow is based on a public certificate handler URL:

* A certification is created through `POST /api/certification`
* The API generates a unique public `certification_id`
* A public handler URL is returned, such as `/certificate/ID/CERT-TR00012-20260413-00004`
* The certificate page uses the `resources/views/certificate.blade.php` Blade file

Important changes in the current version:

* Quiz-based certifications are now **exam-only**
* Each student can have only **one certification per training** and **one certification per exam**
* `POST /api/certification` is **idempotent** for the same `student_id + training_id` or `student_id + quiz_id`
* If the certification already exists, the API returns the existing certification instead of creating a new one, with `already_exists: true` and a message saying that nothing new was generated
* A certification is issued **once** and is never regenerated, even when lessons or quizzes are added to the training afterwards
* Listing endpoints return duplicate-safe results
* Listing endpoints load each certification's `training` with its `badge_logo` file and each `quiz` with its `image`, so a client can draw the badge of a certificate without a second request
* `GET /api/certification/{id}` now returns certification data instead of downloading a PDF
* Archive, old preview URL, and old preview query-page flows are no longer part of the active certification flow
* The certificate payload now includes:
  * `name`
  * `course`
  * `date`
  * `duration`
  * `duration_label`
  * `subject_label`
  * `signatory`
  * `company_logo`
  * `badge_logo`
  * `certification_id`

***

## **Authentication**

Authenticated API endpoints require a **Bearer Token** via Laravel Sanctum:

```text theme={null}
Authorization: Bearer {your_token}
```

The token is obtained from `POST /api/auth/login`.

Public endpoints in the current certification flow:

* `POST /api/certification/handler-url`
* `POST /api/certification/verify`
* `GET /certificate/ID/{certification_id}`
* `GET /certificate/verify`
* `GET /certificate`

***

## **Base URL**

```text theme={null}
https://your-domain.com/api
```

The public web routes do not use the `/api` prefix:

```text theme={null}
https://your-domain.com
```

***

## **Endpoints**

### **1. Get Certified Trainings by Student**

Returns all training-based certifications for a given student.

The response is duplicate-safe: only one certification per student per training is returned.

Each `training` comes with its `badge_logo` file (`null` when the training has no badge of its own; clients then fall back to the company's `default_badge_logo`, endpoint 8).

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/api/certified_trainings_by_student/{student_id}` |
| **Auth** | Bearer Token |
| **Policy** | `viewAny` on `Certification` |

**Example Request**

```text theme={null}
GET /api/certified_trainings_by_student/5
Authorization: Bearer {token}
```

**Success Response** — `200 OK`

```text theme={null}
[
  {
    "id": 1,
    "certification_id": "CERT-TR00012-20260406-00001",
    "student_id": 5,
    "training_id": 12,
    "quiz_id": null,
    "created_at": "2026-04-06 10:30:00",
    "updated_at": "2026-04-06 10:30:00",
    "training": {
      "id": 12,
      "label": "Project Management Professional (PMP)®",
      "badge_logo_id": 91,
      "badge_logo": {
        "id": 91,
        "type": 3,
        "url": "https://pub-xxxx.r2.dev/files/pmp-badge.png"
      }
    },
    "quiz": null
  }
]
```

***

### **2. Get Certified Exams by Student**

Returns all exam-based certifications for a given student.

Current supported URLs:

* Preferred: `/api/certified_exams_by_student/{student_id}`
* Backward-compatible alias: `/api/certified_quizzes_by_student/{student_id}`

Only exam certifications are returned here. Regular quizzes attached to trainings or chapters are excluded.

The response is duplicate-safe: only one certification per student per exam is returned.

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/api/certified_exams_by_student/{student_id}` |
| **Alias** | `/api/certified_quizzes_by_student/{student_id}` |
| **Auth** | Bearer Token |
| **Policy** | `viewAny` on `Certification` |

**Example Request**

```text theme={null}
GET /api/certified_exams_by_student/5
Authorization: Bearer {token}
```

**Success Response** — `200 OK`

```text theme={null}
[
  {
    "id": 3,
    "certification_id": "CERT-EX00008-20260406-00003",
    "student_id": 5,
    "training_id": null,
    "quiz_id": 8,
    "created_at": "2026-04-06 14:00:00",
    "updated_at": "2026-04-06 14:00:00",
    "training": null,
    "quiz": {
      "id": 8,
      "label": "PMP Final Exam",
      "training_id": null,
      "chapter_id": null,
      "image_id": 54,
      "image": {
        "id": 54,
        "type": 3,
        "url": "https://pub-xxxx.r2.dev/files/pmp-exam.png"
      }
    }
  }
]
```

***

### **3. Get Certified Students by Training**

Returns all certifications issued for a specific training.

The response is duplicate-safe: only one certification per student for that training is returned.

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/api/certified_students_by_training/{training_id}` |
| **Auth** | Bearer Token |
| **Policy** | `viewAny` on `Certification` |

**Example Request**

```text theme={null}
GET /api/certified_students_by_training/12
Authorization: Bearer {token}
```

**Success Response** — `200 OK`

```text theme={null}
[
  {
    "id": 1,
    "certification_id": "CERT-TR00012-20260406-00001",
    "student_id": 5,
    "training_id": 12,
    "quiz_id": null,
    "created_at": "2026-04-06 10:30:00",
    "updated_at": "2026-04-06 10:30:00",
    "training": {
      "id": 12,
      "label": "Project Management Professional (PMP)®",
      "badge_logo_id": 91,
      "badge_logo": {
        "id": 91,
        "type": 3,
        "url": "https://pub-xxxx.r2.dev/files/pmp-badge.png"
      }
    },
    "quiz": null
  }
]
```

***

### **4. Get Certified Students by Exam**

Returns all certifications issued for a specific exam.

Current supported URLs:

* Preferred: `/api/certified_students_by_exam/{quiz_id}`
* Backward-compatible alias: `/api/certified_students_by_quiz/{quiz_id}`

Only exam certifications are returned here.

The response is duplicate-safe: only one certification per student for that exam is returned.

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/api/certified_students_by_exam/{quiz_id}` |
| **Alias** | `/api/certified_students_by_quiz/{quiz_id}` |
| **Auth** | Bearer Token |
| **Policy** | `viewAny` on `Certification` |

**Example Request**

```text theme={null}
GET /api/certified_students_by_exam/8
Authorization: Bearer {token}
```

**Success Response** — `200 OK`

```text theme={null}
[
  {
    "id": 3,
    "certification_id": "CERT-EX00008-20260406-00003",
    "student_id": 5,
    "training_id": null,
    "quiz_id": 8,
    "created_at": "2026-04-06 14:00:00",
    "updated_at": "2026-04-06 14:00:00",
    "training": null,
    "quiz": {
      "id": 8,
      "label": "PMP Final Exam",
      "image_id": 54,
      "image": {
        "id": 54,
        "type": 3,
        "url": "https://pub-xxxx.r2.dev/files/pmp-exam.png"
      }
    }
  }
]
```

***

### **5. Get Certifications by User**

Returns all certifications belonging to a specific user, without separating training certifications from exam certifications.

You may search by:

* `user_id`
* `user_email`

Send exactly one of those identifiers.

The response is unified and duplicate-safe: a user can only have one certification per training and one certification per exam.

Each certification embeds its source:

* `training` (training certifications): the training row with its `badge_logo` file. `badge_logo` is `null` when the training has no badge of its own; the client then falls back to the company's `default_badge_logo` (endpoint 8). This is the badge the Flutter Certificates tab draws on each earned certificate.
* `quiz` (exam certifications): the exam row with its `image` file.
* `certification_source`: `training` or `exam`.
* `certification_handler_url`: the public certificate page.

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/api/certifications/by-user` |
| **Auth** | Bearer Token |
| **Policy** | `viewAny` on `Certification` |

**Example Request by ID**

```text theme={null}
GET /api/certifications/by-user?user_id=5
Authorization: Bearer {token}
```

**Example Request by Email**

```text theme={null}
GET /api/certifications/by-user?user_email=student@example.com
Authorization: Bearer {token}
```

**Success Response** — `200 OK`

```text theme={null}
{
  "message": "User certifications fetched successfully.",
  "user": {
    "id": 5,
    "email": "student@example.com",
    "fullname": "Mohamed Amine Ben Taieb"
  },
  "certifications": [
    {
      "id": 4,
      "certification_id": "CERT-TR00012-20260413-00004",
      "student_id": 5,
      "training_id": 12,
      "quiz_id": null,
      "created_at": "2026-04-13 10:30:00",
      "updated_at": "2026-04-13 10:30:00",
      "training": {
        "id": 12,
        "label": "Project Management Professional (PMP)®",
        "slug": "pmp",
        "badge_logo_id": 91,
        "badge_logo": {
          "id": 91,
          "type": 3,
          "url": "https://pub-xxxx.r2.dev/files/pmp-badge.png"
        },
        "archived_at": null,
        "published": true
      },
      "quiz": null,
      "certification_source": "training",
      "certification_handler_url": "https://your-domain.com/certificate/ID/CERT-TR00012-20260413-00004"
    },
    {
      "id": 8,
      "certification_id": "CERT-EX00008-20260414-00008",
      "student_id": 5,
      "training_id": null,
      "quiz_id": 8,
      "created_at": "2026-04-14 09:00:00",
      "updated_at": "2026-04-14 09:00:00",
      "training": null,
      "quiz": {
        "id": 8,
        "label": "PMP Final Exam",
        "slug": "pmp-final-exam",
        "image_id": 54,
        "image": {
          "id": 54,
          "type": 3,
          "url": "https://pub-xxxx.r2.dev/files/pmp-exam.png"
        }
      },
      "certification_source": "exam",
      "certification_handler_url": "https://your-domain.com/certificate/ID/CERT-EX00008-20260414-00008"
    }
  ]
}
```

The `training` and `quiz` objects are the full model rows (the samples above are shortened). A `training` whose `badge_logo_id` is set but whose `badge_logo` is `null` has an archived or deleted badge file.

***

### **6. Create Certification**

Creates a new certification for a student and sends an in-app notification when the certification is newly created.

This endpoint is idempotent for the certification target:

* Repeating the same `student_id + training_id` request returns the existing training certification
* Repeating the same `student_id + quiz_id` request returns the existing exam certification
* No second certification is created for the same student and target

A certification is issued **once per student and training (or exam)** and is never regenerated. It stays valid when the training later gains lessons or quizzes: this endpoint does not re-check completion, so a repeat request simply gets the existing certification back. Two unique database indexes (`student_id + training_id` and `student_id + quiz_id`) guarantee it even under concurrent requests.

Both outcomes answer `200 OK`. Use the `already_exists` flag to tell them apart instead of parsing `message`:

| `already_exists` | **Meaning** | `message` |
| :- | :- | :- |
| `false` | The certification was just created. An in-app notification is sent to the student. | `Certification created successfully.` |
| `true` | Nothing was generated. The body carries the certification the student already holds (same `id`, same `certification_id`, same `created_at`). No notification is sent. | See the table below |

The status is deliberately not `409`: the goal of the request (the student holds a certification) is met, and the client needs the body to display it.

**Message when the certification already exists**

| **Target** | **Caller is the student (`student_id` is the authenticated user)** | **Caller is someone else (staff)** |
| :- | :- | :- |
| Training | `You already hold a certification for this training. A certification is issued only once per training, so no new one was generated.` | `This learner already holds a certification for this training. A certification is issued only once per training, so no new one was generated.` |
| Exam | `You already hold a certification for this exam. A certification is issued only once per exam, so no new one was generated.` | `This learner already holds a certification for this exam. A certification is issued only once per exam, so no new one was generated.` |

This message is translated from the `Accept-Language` header (`en`, `fr`, `ar`; any other value falls back to `en`). The "created" message is always in English.

You must send exactly one of:

* `training_id`
* `quiz_id`

If `quiz_id` is used, it must point to an **exam** only.

| **Field** | **Value** |
| :- | :- |
| **Method** | `POST` |
| **URL** | `/api/certification` |
| **Auth** | Bearer Token |
| **Policy** | `create` on `Certification` |
| **Content-Type** | `application/json` |

**Example Request**

```text theme={null}
POST /api/certification
Authorization: Bearer {token}
```

**Request Body (Training-based)**

```text theme={null}
{
  "student_id": 5,
  "training_id": 12
}
```

**Request Body (Exam-based)**

```text theme={null}
{
  "student_id": 5,
  "quiz_id": 8
}
```

**Validation Rules**

| **Field** | **Rules** |
| :- | :- |
| `student_id` | required, integer, must exist in `users` table |
| `training_id` | nullable, integer, must exist in `trainings` table, required when `quiz_id` is not sent |
| `quiz_id` | nullable, integer, must exist in `quizzes` table, required when `training_id` is not sent, must belong to an exam |

**Success Response** — `200 OK`

```json theme={null}
{
  "message": "Certification created successfully.",
  "already_exists": false,
  "certification": {
    "id": 4,
    "certification_id": "CERT-TR00012-20260413-00004",
    "student_id": 5,
    "training_id": 12,
    "quiz_id": null,
    "created_at": "2026-04-13 09:00:00",
    "updated_at": "2026-04-13 09:00:00",
    "student": {
      "id": 5,
      "first_name": "Mohamed",
      "last_name": "Amine Ben Taieb",
      "fullname": "Mohamed Amine Ben Taieb"
    },
    "training": {
      "id": 12,
      "label": "Project Management Professional (PMP)®",
      "badge_logo_id": 91,
      "duration": 2100
    },
    "quiz": null
  },
  "certification_handler_url": "https://your-domain.com/certificate/ID/CERT-TR00012-20260413-00004",
  "certificate_data": {
    "name": "Mohamed Amine Ben Taieb",
    "course": "Project Management Professional (PMP)®",
    "date": "2026-04-13",
    "duration": 2100,
    "duration_label": "35 hours",
    "subject_label": "training",
    "signatory": "Director General",
    "company_logo": "https://your-domain.com/storage/files/logo.png",
    "badge_logo": "https://your-domain.com/storage/files/pmp-badge.png",
    "certification_id": "CERT-TR00012-20260413-00004"
  }
}
```

**Already Exists Response** — `200 OK`

Returned for every repeat request, including after the training content has changed.

```json theme={null}
{
  "message": "You already hold a certification for this training. A certification is issued only once per training, so no new one was generated.",
  "already_exists": true,
  "certification": {
    "id": 4,
    "certification_id": "CERT-TR00012-20260413-00004",
    "student_id": 5,
    "training_id": 12,
    "quiz_id": null,
    "created_at": "2026-04-13 09:00:00",
    "updated_at": "2026-04-13 09:00:00"
  },
  "certification_handler_url": "https://your-domain.com/certificate/ID/CERT-TR00012-20260413-00004",
  "certificate_data": {
    "name": "Mohamed Amine Ben Taieb",
    "course": "Project Management Professional (PMP)®",
    "date": "2026-04-13",
    "duration": 2100,
    "duration_label": "35 hours",
    "subject_label": "training",
    "signatory": "Director General",
    "company_logo": "https://your-domain.com/storage/files/logo.png",
    "badge_logo": "https://your-domain.com/storage/files/pmp-badge.png",
    "certification_id": "CERT-TR00012-20260413-00004"
  }
}
```

**Validation Error** — `422 Unprocessable Entity`

```text theme={null}
{
  "message": "The given data was invalid.",
  "errors": {
    "quiz_id": [
      "The selected quiz_id must belong to an exam."
    ]
  }
}
```

***

### **7. Get Certification Details**

Returns certification data, the public certificate handler URL, and the certificate payload used by the Blade handler.

This endpoint no longer downloads a PDF.

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/api/certification/{id}` |
| **Auth** | Bearer Token |
| **Policy** | `view` on `Certification` |

**Example Request**

```text theme={null}
GET /api/certification/4
Authorization: Bearer {token}
```

**Success Response** — `200 OK`

```text theme={null}
{
  "message": "Certification fetched successfully.",
  "certification": {
    "id": 4,
    "certification_id": "CERT-TR00012-20260413-00004",
    "student_id": 5,
    "training_id": 12,
    "quiz_id": null,
    "created_at": "2026-04-13 09:00:00",
    "updated_at": "2026-04-13 09:00:00"
  },
  "certification_handler_url": "https://your-domain.com/certificate/ID/CERT-TR00012-20260413-00004",
  "certificate_data": {
    "name": "Mohamed Amine Ben Taieb",
    "course": "Project Management Professional (PMP)®",
    "date": "2026-04-13",
    "duration": 2100,
    "duration_label": "35 hours",
    "subject_label": "training",
    "signatory": "Director General",
    "company_logo": "https://your-domain.com/storage/files/logo.png",
    "badge_logo": "https://your-domain.com/storage/files/pmp-badge.png",
    "certification_id": "CERT-TR00012-20260413-00004"
  }
}
```

***

### **8. Get Global Certification Config**

Retrieves the company-level certification display configuration used by the certificate payload builder.

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/api/certification/get/config` |
| **Auth** | Bearer Token |
| **Policy** | `getGlobalConfig` on `Certification` |

**Example Request**

```text theme={null}
GET /api/certification/get/config
Authorization: Bearer {token}
```

**Success Response** — `200 OK`

```text theme={null}
{
  "signatory": "Director General",
  "company_logo": "https://your-domain.com/storage/files/logo.png",
  "default_badge_logo": "https://your-domain.com/storage/files/custom-badge.png",
  "certificate_verify_url": "https://your-app-domain.com/certificate/verify"
}
```

**Default Response (no config saved)** — `200 OK`

```text theme={null}
{
  "signatory": "Director General",
  "company_logo": "https://i.imgur.com/fdQTIe2.png",
  "default_badge_logo": "https://pub-2bf4aefec74c4b39a06b127992bb033d.r2.dev/files/DEFAULT_BADGE.png",
  "certificate_verify_url": "https://your-app-domain.com/certificate/verify"
}
```

> `default_badge_logo` returns the public URL of the custom badge image uploaded via endpoint **9**. When no value is saved for the company, it falls back to the platform default badge. The same resolution is used as the fallback `badge_logo` in the certificate payload when a training/exam has no badge of its own.

> `certificate_verify_url` is where the app sends "Verify your certificate". Like every `certification_handler_url`, it is built on the backend's `CERTIFICATE_PUBLIC_URL` when that is set — the app's own domain, whose web server proxies `/certificate/*` to the backend, so shared links never show the backend host (see `Certification_Proxy_README.md` in the backend and frontend repositories) — and on `APP_URL` otherwise.

***

### **9. Set Global Certification Config**

Updates the company-level certification configuration. Only the fields below are currently used by the active handler flow.

| **Field** | **Value** |
| :- | :- |
| **Method** | `POST` |
| **URL** | `/api/certification/update/config` |
| **Auth** | Bearer Token |
| **Policy** | `setGlobalConfig` on `Certification` |
| **Content-Type** | `multipart/form-data` |

**Form Data Parameters**

| **Field** | **Type** | **Rules** |
| :- | :- | :- |
| `company_logo` | file | optional, jpeg/png/jpg/gif/svg, max 5MB |
| `signatory` | string | optional, max 255 characters. Send empty to reset to the default (`Director General`) |
| `default_badge_logo` | file | optional, jpeg/png/jpg/gif/svg, max 5MB. Send empty to reset to the platform default badge |

> `default_badge_logo `**is an image upload** (same as the other logos). Upload an image file and its public URL is stored and returned. Sending it empty clears the stored value back to the platform default; omitting it entirely leaves the current value unchanged. This value becomes the fallback badge in the certificate payload when a training/exam has no badge of its own.

**Postman Setup**

1. Set method to `POST`
2. Set body type to `form-data`
3. Add `company_logo` as `File` when uploading a logo
4. Add `signatory` as `Text` when changing the signer label
5. Add `default_badge_logo` as `File` to upload a custom default badge

**Success Response** — `200 OK`

```text theme={null}
{
  "message": "Certification configuration updated successfully.",
  "config": {
    "signatory": "Director General",
    "company_logo": "https://your-domain.com/storage/files/logo.png",
    "default_badge_logo": "https://your-domain.com/storage/files/custom-badge.png"
  }
}
```

***

### **10. Generate Certification Handler URL**

Generates the public certificate handler URL from an existing certification.

You may send either:

* `id`
* `certification_id`

This endpoint is public and does not require authentication.

| **Field** | **Value** |
| :- | :- |
| **Method** | `POST` |
| **URL** | `/api/certification/handler-url` |
| **Auth** | None |
| **Content-Type** | `application/json` |

**Request Body**

```text theme={null}
{
  "id": 4
}
```

**Alternative Request Body**

```text theme={null}
{
  "certification_id": "CERT-TR00012-20260413-00004"
}
```

**Success Response** — `200 OK`

```text theme={null}
{
  "success": true,
  "certification_id": "CERT-TR00012-20260413-00004",
  "certification_handler_url": "https://your-domain.com/certificate/ID/CERT-TR00012-20260413-00004",
  "message": "Certification handler URL generated successfully."
}
```

***

### **11. Verify Certification**

Verifies a certification by public `certification_id`.

This endpoint is public and does not require authentication.

| **Field** | **Value** |
| :- | :- |
| **Method** | `POST` |
| **URL** | `/api/certification/verify` |
| **Auth** | None |
| **Content-Type** | `application/json` |

**Request Body**

```text theme={null}
{
  "certification_id": "CERT-TR00012-20260413-00004"
}
```

**Success Response** — `200 OK`

```text theme={null}
{
  "success": true,
  "message": "Certification has been verified successfully. This certificate matches a valid record in our system.",
  "certification": {
    "id": 4,
    "certification_id": "CERT-TR00012-20260413-00004",
    "student_id": 5,
    "training_id": 12,
    "quiz_id": null
  },
  "certification_handler_url": "https://your-domain.com/certificate/ID/CERT-TR00012-20260413-00004",
  "certificate_data": {
    "name": "Mohamed Amine Ben Taieb",
    "course": "Project Management Professional (PMP)®",
    "date": "2026-04-13",
    "duration": 2100,
    "duration_label": "35 hours",
    "subject_label": "training",
    "signatory": "Director General",
    "company_logo": "data:image/png;base64,...",
    "badge_logo": "data:image/png;base64,...",
    "certification_id": "CERT-TR00012-20260413-00004"
  }
}
```

**Failure Response** — `404 Not Found`

```text theme={null}
{
  "success": false,
  "message": "Certification ID could not be verified. Please check the ID and try again."
}
```

***

### **12. View Certificate Handler (Web Page)**

Renders the public certificate page using `resources/views/certificate.blade.php`.

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/certificate/ID/{certification_id}` |
| **Auth** | None |
| **Response** | HTML page with canvas-based certificate |

**Example Request**

```text theme={null}
GET https://your-domain.com/certificate/ID/CERT-TR00012-20260413-00004
```

**Success Response** — `200 OK`

The page renders a certificate with:

* student full name
* training label or exam label
* duration
* certification date
* signatory
* company logo
* training badge logo or fallback badge
* generated certification ID
* copy certification ID button

**Printing (since 07/10/2026).** The page's print stylesheet (`@media print`) sets `@page { size: A4 landscape; margin: 0 }`, hides the `.no-print` chrome (the action buttons and the details card) and makes the canvas container full-bleed, so the 1123×794 canvas (A4 ratio) prints edge-to-edge on a single sheet with `object-fit: contain` — no browser margins, no blank second page. On screens narrower than 1024px the certificate is shown above the details card.

**Link preview tags**

The page's `<head>` carries Open Graph and Twitter Card tags, so a shared link previews as the certificate on LinkedIn, Facebook, X, WhatsApp or Slack. Link crawlers do not run the page's canvas script, so the preview image is a server-drawn PNG (endpoint 12b). Every URL is built on `CERTIFICATE_PUBLIC_URL` when it is set, else on `APP_URL`.

```html theme={null}
<title>Student Student – Power BI certificate</title>
<link rel="canonical" href="https://your-app-domain.com/certificate/ID/CERT-TR00006-20260618-00001">
<meta property="og:title" content="Student Student – Power BI certificate">
<meta property="og:description" content="Student Student completed the training &quot;Power BI&quot; (36 hours 30 minutes) and earned this certificate on 18 June 2026. Certificate ID CERT-TR00006-20260618-00001, verifiable on https://your-app-domain.com/certificate/verify">
<meta property="og:url" content="https://your-app-domain.com/certificate/ID/CERT-TR00006-20260618-00001">
<meta property="og:image" content="https://your-app-domain.com/certificate/ID/CERT-TR00006-20260618-00001/image.png?v=3f2a9c0d81e4">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
```

When the learner's name, the course or the signatory is written in a right-to-left script, no image is generated: `og:image` is the training badge and `twitter:card` is `summary`.

**Error Response** — `404 Not Found`

If the certification identifier does not exist, Laravel returns the standard not found response/page.

***

### **12b. Certificate Preview Image (PNG)**

The certificate drawn server-side as a 1200×630 PNG: the `og:image` / `twitter:image` of endpoint 12. Same layout as the page's canvas (logo, badge, name, course, duration, date, signatory, certification ID).

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/certificate/ID/{certification_id}/image.png` |
| **Auth** | None |
| **Rate limit** | 60 requests per minute per IP |
| **Response** | `image/png`, 1200×630, `Cache-Control: public, max-age=86400` |

**Example Request**

```text theme={null}
GET https://your-domain.com/certificate/ID/CERT-TR00006-20260618-00001/image.png
```

* Drawn on the first request and cached on the server. The `?v=` that endpoint 12 appends is a fingerprint of the drawn content: it changes when the training, the learner's name, the badge or the signatory change, so social networks fetch the new image. The image itself ignores the query string.
* Under `/certificate/`, so the app-domain proxy described in `Certification_Proxy_README.md` serves it too.

**Error Response** — `404 Not Found`

The certification identifier does not exist, or its texts use a right-to-left script the image cannot render.

***

### **13. View Certification Verification Page**

Renders the public certification verification page using `resources/views/certificate_verify.blade.php`.

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/certificate/verify` |
| **Auth** | None |
| **Response** | HTML page with verification form |

**Example Request**

```text theme={null}
GET https://your-domain.com/certificate/verify
```

**Success Response** — `200 OK`

The page lets users:

* enter a public `certification_id`
* click **Verify Certif**
* see a success or failure message
* render the certificate inline when verification succeeds
* view the duration and certification ID in the rendered certificate

***

### **14. Render Certificate from JSON**

Renders the certificate view using the local development JSON file at `storage/app/certificate_data.json`.

This route is intended for local testing of the Blade view and requires no authentication.

| **Field** | **Value** |
| :- | :- |
| **Method** | `GET` |
| **URL** | `/certificate` |
| **Auth** | None |
| **Response** | HTML page with canvas-based certificate |

**Example Request**

```text theme={null}
GET http://localhost:8000/certificate
```

**Success Response** — `200 OK`

The page renders the same Blade used by the public certification handler.

**Error Response** — `404 Not Found`

```text theme={null}
{
  "message": "certificate_data.json not found."
}
```

***

## **Postman Setup**

### **Step 1 — Environment Variables**

Create a Postman environment with:

| **Variable** | **Value** |
| :- | :- |
| `base_url` | `https://your-domain.com/api` |
| `web_url` | `https://your-domain.com` |
| `token` | *(set after login)* |

### **Step 2 — Login & Save Token**

Create a `POST {{base_url}}/auth/login` request:

```text theme={null}
{
  "email": "admin@example.com",
  "password": "your_password"
}
```

In the **Tests** tab, add this script to auto-save the token:

```text theme={null}
var jsonData = pm.response.json();
pm.environment.set("token", jsonData.token);
```

### **Step 3 — Set Auth Header**

For authenticated certification endpoints:

* Go to **Authorization**
* Type: **Bearer Token**
* Token: `{{token}}`

Do not use auth for:

* `POST {{base_url}}/certification/handler-url`
* `POST {{base_url}}/certification/verify`
* `GET {{web_url}}/certificate/ID/{certification_id}`
* `GET {{web_url}}/certificate/verify`
* `GET {{web_url}}/certificate`

### **Step 4 — Postman Collection Structure**

```text theme={null}
📁 Certifications API
├── 📁 Auth
│   └── POST Login
├── 📁 Certification Records
│   ├── GET  Get Certifications by User          /certifications/by-user?user_id=5
│   ├── POST Create Certification                /certification
│   ├── GET  Get Certification Details           /certification/{id}
│   ├── GET  Certified Trainings by Student      /certified_trainings_by_student/{id}
│   ├── GET  Certified Exams by Student          /certified_exams_by_student/{id}
│   ├── GET  Certified Students by Training      /certified_students_by_training/{id}
│   └── GET  Certified Students by Exam          /certified_students_by_exam/{id}
├── 📁 Global Config
│   ├── GET  Get Config                          /certification/get/config
│   └── POST Set Config (form-data)              /certification/update/config
├── 📁 Public Verification
│   ├── POST Verify Certification                /certification/verify
│   └── GET  Verification Page                   {{web_url}}/certificate/verify
├── 📁 Public Handler
│   ├── POST Generate Handler URL                /certification/handler-url
│   └── GET  Open Handler Page                   {{web_url}}/certificate/ID/{certification_id}
└── 📁 Local Dev
    └── GET  Render from JSON                    {{web_url}}/certificate
```

### **Tips for Testing**

* **Create Certification**: Copy `certification_handler_url` from the API response and open it in a browser
* **Duplicate Protection**: Repeating the same create request for the same student and training/exam returns the existing certification instead of creating a new one
* **Unified User Lookup**: Use `/certifications/by-user` when you want all certifications for a learner, regardless of source
* **Verify Certification**: Use `/certification/verify` from the public verification page or from Postman without auth
* **Handler URL Endpoint**: Useful when you already know the certification database `id` or public `certification_id`
* **Config Uploads**: Use `form-data` and set `company_logo` as `File`
* **Exam Certifications**: Always use an exam `quiz_id`, not a training/chapter quiz

***

## **Certificate Payload Reference**

The public certificate handler and the local render route both use the following payload structure:

```text theme={null}
{
  "name": "Mohamed Amine Ben Taieb",
  "course": "Project Management Professional (PMP)®",
  "date": "2026-04-06",
  "duration": 2100,
  "duration_label": "35 hours",
  "subject_label": "training",
  "signatory": "Director General",
  "company_logo": "https://i.imgur.com/fdQTIe2.png",
  "badge_logo": "https://www.pmi.org/-/media/pmi/other-images/certifications/digital-badges/pmp-badge.png",
  "certification_id": "CERT-TR00012-20260406-00042"
}
```

For local testing, `GET /certificate` reads from:

```text theme={null}
storage/app/certificate_data.json
```

| **Field** | **Type** | **Description** |
| :- | :- | :- |
| `name` | string | Full name of the certificate recipient |
| `course` | string | Training label or exam label |
| `date` | string | Certification issue date in `YYYY-MM-DD` format |
| `duration` | integer | Raw duration value used by the certificate payload |
| `duration_label` | string | Human-readable formatted duration |
| `subject_label` | string | Usually `training` or `exam` |
| `signatory` | string | Signer name or title from `CertificationConfig` |
| `company_logo` | string (URL) | Company logo used by the certificate handler |
| `badge_logo` | string (URL) | Training badge logo, exam image fallback, then the company's configured `default_badge_logo` (endpoint 9), then the platform default badge |
| `certification_id` | string | Public unique identifier generated from the certification record |

Image URLs are converted server-side to base64 data URLs when rendering the Blade, to avoid browser-side CORS issues.

***

## **Error Handling**

All endpoints follow a consistent error format:

| **Status** | **Meaning** |
| :- | :- |
| `200` | Success |
| `401` | Unauthenticated — missing or invalid Bearer token |
| `403` | Forbidden — user lacks the required policy permission |
| `404` | Resource not found |
| `422` | Validation error — check the `errors` object |
| `500` | Server error — check the `message` field |

**Standard error shape**

```text theme={null}
{
  "message": "Human-readable error description",
  "errors": {
    "field_name": ["Validation message"]
  }
}
```

The `errors` object is only present for validation failures.


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