> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dokstamp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Syncing Academic Structure

> Step-by-step guide for syncing courses, modules, cohorts, students, and certificates from a third-party system to DokStamp.

# Syncing Academic Structure

This page covers how to sync each academic entity from your system to DokStamp, in the correct dependency order.

<Info>
  Always follow the order below. Creating a certificate before its dependencies exist will result in `422` validation errors.
</Info>

***

## Sync order

| Step | Event in your system                | DokStamp action                            |
| ---- | ----------------------------------- | ------------------------------------------ |
| 1    | Institution configured              | `POST /institutions` (or manual setup)     |
| 2    | Course created / updated            | `POST /courses` or `PATCH /courses/{uuid}` |
| 3    | Module/discipline created / updated | `POST /modules` or `PATCH /modules/{uuid}` |
| 4    | Module added to course              | `POST /courses/{uuid}/attach/modules`      |
| 5    | Modules organized into groups       | `POST /courses/{uuid}/modules/groups`      |
| 6    | Cohort (class intake) created       | `POST /cohorts`                            |
| 7    | Student becomes eligible            | lookup → `POST /students` (if new)         |
| 8    | Student enrolled in course          | `POST /enrollments`                        |
| 9    | Certificate issuance triggered      | `POST /files` + `POST /certificates`       |

***

## Step 2 — Syncing a course

When a course is created or updated in your system, mirror the change to DokStamp.

<CodeGroup>
  ```javascript Create theme={null}
  async function syncCourse(course) {
    // Search by code to check if it already exists
    const existing = await api.get('/courses', {
      params: { 'where[code]': course.code }
    });

    if (existing.data.length > 0) {
      // Update existing
      await api.patch(`/courses/${existing.data[0].uuid}`, {
        name: course.name,
        description: course.description,
        workload_hours: course.workloadHours,
        status: course.isActive ? 'active' : 'archived',
      });
      return existing.data[0].uuid;
    }

    // Create new
    const res = await api.post('/courses', {
      name: course.name,
      code: course.code,
      institution_uuid: INSTITUTION_UUID,
      workload_hours: course.workloadHours,
      status: 'active',
    });
    return res.data.uuid;
  }
  ```

  ```php PHP theme={null}
  function syncCourse(array $course): string
  {
      $existing = Http::withToken($token)
          ->withHeaders(['X-Tenant' => $tenant])
          ->get('/courses', ['where[code]' => $course['code']])
          ->json('data');

      if (!empty($existing)) {
          Http::withToken($token)
              ->withHeaders(['X-Tenant' => $tenant])
              ->patch("/courses/{$existing[0]['uuid']}", [
                  'name'          => $course['name'],
                  'workload_hours'=> $course['workload_hours'],
                  'status'        => $course['is_active'] ? 'active' : 'archived',
              ]);
          return $existing[0]['uuid'];
      }

      return Http::withToken($token)
          ->withHeaders(['X-Tenant' => $tenant])
          ->post('/courses', $course)
          ->json('data.uuid');
  }
  ```
</CodeGroup>

***

## Step 3 — Syncing modules/disciplines

```javascript theme={null}
async function syncModule(module) {
  const existing = await api.get('/modules', {
    params: {
      'where[code]': module.code,
      'where[institution][uuid]': INSTITUTION_UUID,
    }
  });

  if (existing.data.length > 0) {
    await api.patch(`/modules/${existing.data[0].uuid}`, {
      name: module.name,
      workload: module.workload,
      credits: module.credits,
    });
    return existing.data[0].uuid;
  }

  const res = await api.post('/modules', {
    name: module.name,
    code: module.code,
    institution_uuid: INSTITUTION_UUID,
    workload: module.workload,
    credits: module.credits,
    level: module.level,       // 'undergraduate' | 'graduate' | 'technical' | 'open'
    modality: module.modality, // 'in_person' | 'online' | 'hybrid'
  });
  return res.data.uuid;
}
```

***

## Step 4 — Attaching modules to a course

After creating/syncing both the course and its modules, attach them:

```javascript theme={null}
await api.post(`/courses/${courseUuid}/attach/modules`, {
  modules: moduleUuids.map((module_uuid, index) => ({
    module_uuid,
    order: index + 1,
    is_required: true,
  }))
});
```

<Warning>
  Re-attaching a module that is already attached to a course will return a `422`. To check which modules are already attached, use `GET /courses/{uuid}?includes[modules]=1` before attaching.
</Warning>

***

## Step 5 — Module groups (optional)

If your system organizes disciplines into semesters or periods, mirror that structure:

```javascript theme={null}
async function syncModuleGroup(courseUuid, group) {
  const res = await api.post(`/courses/${courseUuid}/modules/groups`, {
    name: group.name,   // e.g. "1st Semester", "Core Modules"
    order: group.order,
  });
  return res.data.uuid;
}
```

Then re-attach modules specifying the group:

```javascript theme={null}
await api.post(`/courses/${courseUuid}/attach/modules`, {
  modules: [{
    module_uuid: moduleUuid,
    order: 1,
    course_module_group_uuid: groupUuid,
  }]
});
```

***

## Step 6 — Syncing cohorts

A cohort maps to a specific graduating intake (e.g., "Evening Class 2024/1"):

```javascript theme={null}
async function syncCohort(cohort) {
  const existing = await api.get('/cohorts', {
    params: { 'where[code]': cohort.code }
  });

  if (existing.data.length > 0) return existing.data[0].uuid;

  const res = await api.post('/cohorts', {
    course_uuid: cohort.courseUuid,
    code: cohort.code,
    modality: cohort.modality,
    start_date: cohort.startDate,
    end_date: cohort.endDate,
  });
  return res.data.uuid;
}
```

***

## Step 7 — Registering eligible students

When your system determines a student is eligible for a certificate, register them in DokStamp (or verify they already exist):

```javascript theme={null}
async function syncStudent(student) {
  // Always search by email first
  const existing = await api.get('/students', {
    params: { 'where[user][email]': student.email }
  });

  if (existing.data.length > 0) return existing.data[0].uuid;

  const res = await api.post('/students', {
    name: student.name,
    email: student.email,
    date_of_birth: student.dateOfBirth, // YYYY-MM-DD
    gender: student.gender,
  });
  return res.data.uuid;
}
```

***

## Step 8 — Creating the enrollment

```javascript theme={null}
const enrollment = await api.post('/enrollments', {
  student_uuid: studentUuid,
  course_uuid: courseUuid,
  cohort_uuid: cohortUuid,       // optional
  enrolled_at: student.enrolledAt,
  completion_status: 'completed',
  grade: student.finalGrade,
  completed_at: student.completedAt,
});
```

***

## Step 9 — Issuing the certificate

```javascript theme={null}
// 1. Upload the PDF
const fileRes = await api.post('/files', formDataWithPdf);
const fileUuid = fileRes.data[0].uuid;

// 2. Issue the certificate
// Passing finish: true immediately issues the certificate, signs all files,
// generates the academic transcript, and notifies the student.
const cert = await api.post('/certificates', {
  institution_uuid: INSTITUTION_UUID,
  course_uuid: courseUuid,
  student_uuid: studentUuid,
  cohort_uuid: cohortUuid,       // optional
  enrollment_uuid: enrollmentUuid, // optional
  file_uuid: fileUuid,
  finish: true,
});

console.log('Verification URL:', cert.data.public_verification_url);
```

<Info>
  `finish: true` triggers the full issuance pipeline in a single call: the certificate status is set to `issued`, all files are cryptographically signed, an academic transcript is generated, and the student is notified. Omitting `finish` (or passing `false`) creates the certificate as a `draft` for later review.
</Info>

***

## Full async example

For queue-based integrations, wrap each step in a job:

```javascript theme={null}
// jobs/SyncCourseJob.js
export async function handle({ course }) {
  try {
    const uuid = await syncCourse(course);
    await syncModules(uuid, course.modules);
    logger.info(`Course synced: ${uuid}`);
  } catch (err) {
    if (err.response?.status >= 500 || err.response?.status === 429) {
      throw err; // Will be retried by the queue
    }
    logger.error(`Sync failed (no retry): ${err.message}`, { course });
  }
}
```

Retry on `5xx` and `429`. Log and discard `4xx` (they indicate a data problem, not a transient failure).
