Skip to main content

Syncing Academic Structure

This page covers how to sync each academic entity from your system to DokStamp, in the correct dependency order.
Always follow the order below. Creating a certificate before its dependencies exist will result in 422 validation errors.

Sync order

StepEvent in your systemDokStamp action
1Institution configuredPOST /institutions (or manual setup)
2Course created / updatedPOST /courses or PATCH /courses/{uuid}
3Module/discipline created / updatedPOST /modules or PATCH /modules/{uuid}
4Module added to coursePOST /courses/{uuid}/attach/modules
5Modules organized into groupsPOST /courses/{uuid}/modules/groups
6Cohort (class intake) createdPOST /cohorts
7Student becomes eligiblelookup → POST /students (if new)
8Student enrolled in coursePOST /enrollments
9Certificate issuance triggeredPOST /files + POST /certificates

Step 2 — Syncing a course

When a course is created or updated in your system, mirror the change to DokStamp.
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;
}

Step 3 — Syncing modules/disciplines

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:
await api.post(`/courses/${courseUuid}/attach/modules`, {
  modules: moduleUuids.map((uuid, index) => ({
    uuid,
    order: index + 1,
    is_required: true,
  }))
});
Attaching is idempotent for new modules, but re-attaching an already-attached module will return a 422. Query GET /courses/{uuid}/attach/modules first to get the list of modules not yet attached.

Step 5 — Module groups (optional)

If your system organizes disciplines into semesters or periods, mirror that structure:
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:
await api.post(`/courses/${courseUuid}/attach/modules`, {
  modules: [{
    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”):
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):
async function syncStudent(student) {
  // Always search by email first
  const existing = await api.get('/students', {
    params: { 'where[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

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

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

// 2. Issue the certificate
const cert = await api.post('/certificates', {
  institution_uuid: INSTITUTION_UUID,
  course_uuid: courseUuid,
  student_uuid: studentUuid,
  cohort_uuid: cohortUuid,
  enrollment_uuid: enrollmentUuid,
  file_uuid: fileUuid,
  status: 'issued',
  issued_at: new Date().toISOString(),
});

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

Full async example

For queue-based integrations, wrap each step in a job:
// 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).