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
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((module_uuid, index) => ({
module_uuid,
order: index + 1,
is_required: true,
}))
});
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.
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: [{
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”):
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[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
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
// 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);
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.
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).