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