| name | bc-certificate-manage-and-assign-specialties |
| description | Use when academy staff need to list or view certificate specialties, create or update academy-owned specialties, link a syllabus to a specialty, or assign a specialty to a student by issuing a certificate; do NOT use for listing a student's own certificates (use certificate me endpoint). |
| requires | [] |
Skill: Manage and Assign Certificate Specialties
When to Use
Use this skill when the user asks to list or view academy specialties, see which specialties are offered or which syllabi a specialty uses, to create a new specialty, edit an existing specialty (academy-owned), to link a syllabus to a specialty, or to give a specialty to a student (issue a certificate). Do not use for a student listing their own certificates (that is a different endpoint).
Concepts
- Specialty: A certificate type (e.g. "Full-Stack Web Development"). In the API it is the certificate the student receives. A specialty can optionally belong to an academy (
academy_id). Academy-owned specialties can be created and updated by staff with crud_certificate; global specialties (no academy) are read-only for academy staff. A specialty is linked to one or many syllabi via the syllabuses (ManyToMany); the response also includes syllabus (first of syllabuses) for compatibility.
- Assigning a specialty to a student: Issuing a certificate — creating a user-specialty record that states the student has completed the specialty. The certificate is tied to a cohort and the cohort's syllabus. The system picks a single specialty for that syllabus using cohort academy first, then global specialties (
academy_id null): it uses a specialty that lists the cohort's syllabus in syllabuses and whose academy_id equals the cohort's academy; if none, it uses a global specialty that lists that syllabus. If neither exists, issuance fails with missing-specialty.
- Completion / grading strategy: Certificate eligibility uses
evaluate_cohort_user_completion(cohort_user) from breathecode/admissions/services/completion.py. The evaluator reads grading_strategy.completion from the cohort syllabus and falls back to legacy mandatory PROJECT completion when no strategy exists. See GRADING_STRATEGY_COMPLETION.md.
- Syllabus–specialty link uniqueness: For each syllabus, at most one specialty per academy bucket may list it: one specialty with
academy_id = A (per academy), and at most one global specialty (academy_id null). Different academies may each have their own specialty linked to the same syllabus. This is enforced when linking via the API and when editing the M2M in Django admin.
- Managing specialties: Staff can list specialties, create academy-owned specialties (POST), update academy-owned specialties (PUT/PATCH when
specialty.academy_id matches the request academy), and link a syllabus to a specialty (POST specialty/:id/syllabus, requires crud_syllabus). The source of truth for "which syllabi a specialty uses" is syllabuses; the response includes syllabus (first of syllabuses) and syllabuses.
Workflow
-
Create a specialty (academy-owned). Call POST /v1/certificate/academy/specialty with Authorization, Academy: <academy_id>, and capability crud_certificate. Body: at least name and slug; optionally description, logo_url, duration_in_hours, expiration_day_delta, status. Response 201 with the created specialty (id, slug, name, status, academy, syllabus, syllabuses, created_at, updated_at).
-
Update a specialty. Call PUT or PATCH /v1/certificate/academy/specialty/<specialty_id> with Authorization, Academy: <academy_id>, and capability crud_certificate. Allowed only when the specialty's academy_id equals the request academy. Body: fields to update (name, slug, description, status, etc.). Response 200 with the updated specialty.
-
Link a syllabus to a specialty. Call POST /v1/certificate/academy/specialty/<specialty_id>/syllabus with Authorization, Academy: <academy_id>, and capability crud_syllabus. Body: { "syllabus_id": <id> } or { "syllabus_slug": "<slug>" }. Response 200 or 201 with the updated specialty (syllabuses list includes the newly linked syllabus). If another specialty in the same bucket (same academy_id, or both global) already links that syllabus, the API returns 400 with detail / slug syllabus-specialty-already-linked. If the syllabus is already linked to this specialty, the call is idempotent (200).
-
List specialties offered by the academy. Call GET /v1/certificate/academy/specialty with Authorization and Academy: <academy_id>. The list includes global specialties (academy_id null — apply to all academies) and specialties owned by the academy. Specialties belonging to other academies are excluded. Response includes each specialty's id, slug, name, status, academy, syllabus (first of syllabuses), syllabuses. Use query syllabus_slug=<slug> to filter by syllabus, or like=<text> to search by name.
-
Get one specialty. Call GET /v1/certificate/academy/specialty/<specialty_id> with Authorization and Academy: <academy_id>. Response 200 with the full specialty object (or 404 if not found or not visible to the academy).
-
Assign a specialty to one student (issue one certificate). Call POST /v1/certificate/cohort/<cohort_id>/student/<student_id>. The student must be in that cohort with role STUDENT. The cohort must have a syllabus assigned; that syllabus must be in the specialty's syllabuses. Optional body: { "layout_slug": "default" }. Response is 201 with the certificate (user_specialty) object. The student must meet completion, graduation and financial rules (e.g. completion strategy satisfied, GRADUATED, FULLY_PAID or UP_TO_DATE, cohort ENDED if applicable).
-
Issue certificates for an entire cohort. Call POST /v1/certificate/cohort/<cohort_id> with Authorization and Academy: <academy_id>. Optional body: { "layout_slug": "default" }. All students in that cohort (role STUDENT) receive a certificate for the cohort's specialty. The cohort must have a syllabus linked to a specialty (via syllabuses); cohort stage must be ENDED (or never_ends). Response is 201 with a list of certificate objects.
-
List or filter issued certificates (academy). Call GET /v1/certificate/ (root) with Authorization and Academy: <academy_id>. Optional query: user_id=<id> or like=<name> to filter. Use this to verify who has been assigned which specialty.
Prerequisite: The cohort's syllabus must appear in a specialty's syllabuses for that cohort's academy (academy-owned specialty) or in a global specialty. If issuing fails with "Specialty has no Syllabus assigned" or "missing-specialty", link the syllabus to an appropriate specialty (POST specialty/:id/syllabus) — e.g. an academy-owned specialty for that academy, or a global one — then retry.
Endpoints
| Action | Method | Path | Headers | Body / Query | Response |
|---|
| List academy specialties | GET | /v1/certificate/academy/specialty | Authorization, Academy: <academy_id> | Optional: syllabus_slug=<slug>, like=<text> | List of specialties (id, slug, name, status, academy, syllabus, syllabuses). |
| Create specialty | POST | /v1/certificate/academy/specialty | Authorization, Academy: <academy_id> | name, slug; optional: description, logo_url, duration_in_hours, expiration_day_delta, status | 201, specialty object. |
| Get one specialty | GET | /v1/certificate/academy/specialty/<specialty_id> | Authorization, Academy: <academy_id> | — | Specialty object or 404. |
| Update specialty | PUT / PATCH | /v1/certificate/academy/specialty/<specialty_id> | Authorization, Academy: <academy_id> | Fields to update | 200, specialty object. |
| Link syllabus to specialty | POST | /v1/certificate/academy/specialty/<specialty_id>/syllabus | Authorization, Academy: <academy_id> | syllabus_id or syllabus_slug | 200/201, specialty object; 400 syllabus-specialty-already-linked if another specialty in the same academy/global bucket already uses that syllabus. |
| Issue certificate for one student | POST | /v1/certificate/cohort/<cohort_id>/student/<student_id> | Authorization, Academy: <academy_id> | Optional; see request sample. | 201, certificate (user_specialty) object. |
| Issue certificates for cohort | POST | /v1/certificate/cohort/<cohort_id> | Authorization, Academy: <academy_id> | Optional; see request sample. | 201, list of certificate objects. |
| Get one student's certificate | GET | /v1/certificate/cohort/<cohort_id>/student/<student_id> | Authorization, Academy: <academy_id> | — | Certificate object or 404. |
| List academy certificates | GET | /v1/certificate/ | Authorization, Academy: <academy_id> | Optional: user_id=<id>, like=<name>, sort | Paginated list of issued certificates. |
| Delete certificates (bulk) | DELETE | /v1/certificate/ | Authorization, Academy: <academy_id> | Query: id=1&id=2 (ids of user_specialty) | 204 No Content. |
Capabilities: read_certificate for GET (list and get one specialty, certificates). crud_certificate for create/update specialty and for certificate issuance/delete. crud_syllabus for linking a syllabus to a specialty.
Create specialty — request (POST /v1/certificate/academy/specialty):
{
"name": "Full-Stack Web Development",
"slug": "full-stack",
"description": "Complete full-stack program",
"status": "ACTIVE"
}
Create specialty — response (201):
{
"id": 1,
"slug": "full-stack",
"name": "Full-Stack Web Development",
"academy": {"id": 1, "slug": "academy-slug", "name": "Academy Name"},
"syllabus": null,
"syllabuses": [],
"status": "ACTIVE",
"created_at": "2026-03-13T12:00:00Z",
"updated_at": "2026-03-13T12:00:00Z"
}
Link syllabus — request (POST /v1/certificate/academy/specialty/<specialty_id>/syllabus):
{
"syllabus_id": 5
}
Or { "syllabus_slug": "full-stack" }.
Link syllabus — response (200/201): Same shape as get specialty; syllabuses array includes the newly linked syllabus.
Issue certificate for one student — request (POST /v1/certificate/cohort/<cohort_id>/student/<student_id>):
{
"layout_slug": "default"
}
Body is optional; omit or use "layout_slug": "default" if the academy has a default layout.
Issue certificate for one student — response (201):
{
"id": 200,
"user": {"id": 50, "first_name": "Jane", "last_name": "Doe"},
"specialty": {"id": 1, "slug": "full-stack", "name": "Full-Stack Web Development", "academy": null, "syllabus": {"id": 5, "name": "...", "slug": "..."}, "syllabuses": [{"id": 5, "name": "...", "slug": "..."}]},
"cohort": {"id": 10, "name": "Cohort Jan 2026"},
"layout": {"slug": "default"},
"status": "PERSISTED",
"created_at": "2026-03-11T12:00:00Z"
}
Issue certificates for cohort — request (POST /v1/certificate/cohort/<cohort_id>):
{
"layout_slug": "default"
}
Issue certificates for cohort — response (201):
[
{
"id": 200,
"user": {"id": 50, "first_name": "Jane", "last_name": "Doe"},
"specialty": {"id": 1, "slug": "full-stack", "name": "Full-Stack Web Development"},
"cohort": {"id": 10, "name": "Cohort Jan 2026"},
"status": "PERSISTED"
}
]
Edge Cases
- specialty not found (404): Get/update/link when
specialty_id is invalid or the specialty is not visible to the academy. Tell the user to check the id.
- academy cannot update this specialty: The specialty is global (
academy_id null) or belongs to another academy. Only create/update when specialty.academy_id == request academy. Tell the user they can only update specialties that belong to their academy.
- syllabus already linked (same specialty): When linking a syllabus that is already in this specialty's syllabuses, the endpoint is idempotent (returns 200 with the same specialty).
- syllabus-specialty-already-linked (400): Another specialty in the same bucket (same
academy_id, or both global) already lists this syllabus. Tell the user to unlink from the other specialty or use the existing specialty; the same syllabus may still be linked to specialties under other academies.
- missing-specialty / Specialty has no Syllabus assigned: No specialty applies for this cohort's syllabus under the academy-first, then global rule (e.g. only a specialty owned by a different academy lists the syllabus). Link the syllabus to an academy-owned specialty for the cohort's academy or to a global specialty; then retry.
- missing-syllabus-version / The cohort has no syllabus assigned: The cohort has no syllabus. Tell the user to set a syllabus (and syllabus version) for the cohort via the API first, then retry.
- student-not-found / Student not found for this cohort: The user is not a STUDENT in that cohort or the cohort does not belong to the academy. Verify cohort_id and student_id; do not retry the same pair.
- already-exists / This user already has a certificate created: The student already has an issued (PERSISTED) certificate for that cohort. Tell the user; no need to issue again unless they want to reattempt (use reattempt flow if available).
- bad-educational-status / bad-finantial-status / cohort-without-status-ended / with-pending-tasks: The student or cohort does not meet issuance rules.
with-pending-tasks-* now refers to pending assets required by the active completion strategy, not only mandatory projects. Tell the user the exact slug or message; they must fix the student or cohort state before retrying.
- no-default-layout / No layout was specified: The academy has no default certificate layout. Tell the user to pass a valid
layout_slug in the request body or ensure a default layout is set for the academy.
- without-main-teacher: The cohort has no TEACHER. Tell the user to assign a main teacher to the cohort via the API before issuing.
Diagnostics
Use diagnostics when certificate issuance does not happen automatically or when staff needs an explicit reason before fixing cohort/student state.
- Endpoint:
GET /v1/certificate/diagnostic
- Headers:
Authorization, Academy: <academy_id>
- Capability:
read_certificate
- Required query:
kind=graduation or kind=certificate
Targeting options (one mode at a time):
cohort_user_id=<id> (single student-cohort relation)
user_id=<id> or user_email=<email> with optional cohort_id=<id>
scope=cohort&cohort_id=<id> (batch students in cohort; supports limit, offset)
all_graduated_without_certificate=true (certificate kind only; supports limit)
Response shape:
- Single target:
{ "kind", "academy_id", "result": {...} }
- Batch target:
{ "kind", "academy_id", "results": [{...}] }
- Each diagnostic object includes
cohort_user_id, user_id, cohort_id, checks, issues, warnings, and summary.
Checklist
- To create a new specialty: call
POST /v1/certificate/academy/specialty with name and slug (and optional fields); requires crud_certificate.
- To update an academy-owned specialty: call
PUT or PATCH /v1/certificate/academy/specialty/<id>; only when the specialty belongs to the academy.
- To link a syllabus to a specialty: call
POST /v1/certificate/academy/specialty/<id>/syllabus with syllabus_id or syllabus_slug; requires crud_syllabus.
- To see which specialties are offered: call
GET /v1/certificate/academy/specialty with Academy header; use syllabus_slug or like if needed.
- To assign a specialty to one student: call
POST /v1/certificate/cohort/<cohort_id>/student/<student_id>; ensure the cohort's syllabus is linked to a specialty.
- To issue certificates for a full cohort: call
POST /v1/certificate/cohort/<cohort_id>; cohort must be ENDED and have syllabus linked to a specialty.
- If issuance fails with missing-specialty or missing-syllabus-version: tell the user the cohort's syllabus must be linked to a specialty (or cohort must have a syllabus); then retry.
- To list who has been assigned which specialty: call
GET /v1/certificate/ with optional user_id or like; to remove a certificate use DELETE /v1/certificate/?id=....