Skip to content

Competency data

An issuance template (achievement) carries three kinds of competency data: the criteria, the skill tags, and the description engraved on the certificate (imprintDescription). This guide explains how that data appears in the API and how it is frozen at issuance time.

One principle: there is exactly one frozen record

Section titled “One principle: there is exactly one frozen record”

In Kolleges the only record that never changes is the signed credential (VC-JWT). A template’s criteria, tags and description are live data you can edit at any time; at the moment of issuance, the values as they are then are signed into the credential. Editing the template afterwards does not move credentials that were already issued.

So the values the template API returns are both “what is stored now” and “what the next credential will carry”. To keep those two identical, template reads are computed with the same functions the issuer uses when it builds a credential.

Reading a template (Open Badges 3.0 shape)

Section titled “Reading a template (Open Badges 3.0 shape)”

The detail from GET /v2/clubs/{clubDomain}/achievements/{id} carries these fields.

Field Meaning Where it lands in the credential
tag The normalised skill keyword set (trimmed, empties dropped, de-duplicated) achievement.tag[]
alignment[] The same set expressed as /skills/{tag} alignments achievement.alignment[]
achievementType The internal type mapped to an OB3 term (e.g. completionCertificateOfCompletion) achievement.achievementType
criteria.narrative The criteria as one numbered sentence achievement.criteria.narrative
criteria.requirements[] Every requirement, in (order, id) order The numbering of the narrative
coIssuers[] Every co-issuer, representative first x-partnerOrganizations
program The program { name, url }, only when public Not engraved (web guidance only)
imprintDescription · imprintDescriptionEn The description engraved on the certificate image The certificate image
issuedCount Number of completed issuances Not engraved

alignment[], achievementType and criteria.narrative are read-only derived values. They do not exist in the write API; they are computed from the stored tags, type and requirements with the issuer’s own rules. Do not recompute them, use what you read.

List items (GET …/achievements) also carry tag, alignment, achievementType and issuedCount.

Writing a template: what cannot be engraved is not accepted

Section titled “Writing a template: what cannot be engraved is not accepted”

The body of POST …/achievements and PATCH …/achievements/{id} has the same shape as the read. Every validation rule comes from a single question: can the issuer engrave this value?

  • description is required. Open Badges 3.0 requires achievement.description, and the issuer refuses to sign when it is empty. PATCH cannot clear it either.
  • tag items are trimmed and de-duplicated on save. Empty items and items containing a comma are rejected with 422 (errors[].message is tag-contains-comma). At most 30 items in the stored set, 50 characters each.
  • nameEn, descriptionEn and imprintDescriptionEn accept Latin script only. Mixed-in Hangul is rejected with 422 (en-text-not-latin), so that the English slots never hold a value that could not be engraved. An empty string is stored as “none”.
  • criteria.requirements[] uses array position as order; clients do not send order. Each item needs a description or a url (both empty → 422 requirement-empty). type is one of 16 codes.
  • Sending criteria.requirements on PATCH replaces them in full. Delete then insert in one transaction, so requirement ids change on every replace. An empty array deletes them all.
  • program is an object; PATCH merges only the keys you send.
  • If you send description without imprintDescription, the engraved description follows the new description only when it was a mirror of the old one. A separately written engraved description is left alone.
// POST /v2/clubs/{clubDomain}/achievements
{
"name": "Frontend Bootcamp Certificate",
"type": "completion",
"description": "Issued to graduates who completed the 12-week course and presented a final project.",
"tag": ["React", "TypeScript", "Teamwork"],
"nameEn": "Frontend Bootcamp Certificate",
"criteria": {
"requirements": [
{ "type": "course_complete", "description": "Complete the 12-week curriculum" },
{ "type": "presentation_publication", "description": "Present the final project", "url": "https://acme.example/demo-day" }
]
},
"program": { "name": "Frontend Bootcamp, cohort 5", "url": "https://acme.example/bootcamp", "isPublic": true }
}

The response is the detail right after saving, and its criteria.narrative is engraved verbatim into the next credential.

1. Complete the 12-week curriculum
2. Present the final project (https://acme.example/demo-day)

Criteria are ordered (order ASC, id ASC) wherever you read them: the numbering of the narrative, the detail’s criteria.requirements[], and the issuance detail’s form.requirements[] all use the same order. Items that share an order are settled by id, so the order never wobbles.

v1 only adds keys. Existing responses are unchanged, and the following were added (/docs v1.2).

  • Issuance GET: credential { id, shareUrl, verifyUrl } | null, evidences[].isPublic, form.requirements[].id, form.nameEn, form.descriptionEn
  • Template GET: institutions[].role, institutions[].isVerified, requirements[].id, nameEn, descriptionEn
  • Bulk issue response: issued[], failed[]
  • Requirement type input: 8 codes became 16

On v1 the criteria are a requirements[] array and you send order yourself. New product features arrive on /v2 first.