Skoolsewa - School Management System Docs
Developer ResourcesID Cards

ID Cards API Reference

Complete API contracts for ID Cards, including routes, auth, DTOs, responses, errors, examples, and integration notes.

ID Cards - API Reference

Audience: Frontend engineers (the admin panel), backend engineers, QA, and any consumer scanning or verifying a printed card. Scope: Admin surfaces owned by the ID Cards module, plus one unauthenticated public verification surface. All routes are under the admin OpenAPI document (/api/api-docs) except the public verification routes, which sit outside /api/mobile and are still part of the admin document per ADMIN_MODULES.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllersapps/api/src/modules/id-cards/**/*.controller.ts (10 files)Routes, methods, guards, decorators, permissions
DTOsapps/api/src/modules/id-cards/**/dto/*.tsRequest, query, response shapes
Servicesapps/api/src/modules/id-cards/**/*.service.tsBehavior, side effects, errors
Schemapackages/db/src/schema/school/id-cards.tsIDs, enums, persisted fields, constraints
Error registryapps/api/src/common/types/error-codes.ts (ID_CARD_*)46 error codes
Rate limitsapps/api/src/common/guards/ip-throttler.config.tsPUBLIC_CARD_VERIFY, ID_CARD_SCAN, ID_CARD_JOB_SUBMIT
Permissionspackages/db/src/authorization/permission-catalog.ts10 IdCard* modules
Module wiringapps/api/src/app.module.ts, apps/api/src/config/swagger/swagger-documents.tsMembership in ADMIN_MODULES

2. Module Summary

FieldValue
Module nameIdCards
Module slugid-cards
Primary actorsadmin (school staff with granted permissions), guest (the public verification page only)
API surfacesAdmin (/api/id-card-*), public (/api/id-card-verification/*)
Base route prefixes/api/id-card-templates, /api/id-card-template-assignments, /api/id-card-assets, /api/id-card-settings, /api/id-card-generation, /api/id-card-generation-jobs, /api/id-cards, /api/id-card-scan, /api/id-card-verification
Auth modelJwtAuthGuard (global) + RoleGuard with @Permissions("Module_ACTION") on every admin route; the public verification controller carries @Public() and no guard beyond the rate limiter
PersistencePostgreSQL (Drizzle), BullMQ (@skoolsewa/jobs, QueueName.ID_CARDS), file bytes via @skoolsewa/storage
Runtime source of truthid_cards.status (stored); the live verdict shown to any caller is derived from status + validity dates against the school's current date
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
publicIdExternal UUID for a card, template, template version, assignment, asset, or generation job — never the internal PKid-cards.ts schemaEvery route param named publicId
statusThe card lifecycle: generated/printed/active/expired/lost/stolen/damaged/replaced/cancelled/deactivatedid_cards.statusList/detail responses, every lifecycle route
verdictThe DERIVED live meaning of a card: valid/not_yet_valid/expired/not_issued/replaced/lost/stolen/damaged/cancelled/deactivated — computed from status + dates against today, never storedcardVerdict() in id-card-rules.util.tsDetail, scan, and public-verify responses
cardTypestudent/teacher/staff — fixes a template's field catalogue and a card's placement columnsid_card_type enumTemplates, generation, cards
templateStatusdraft/active/inactive/archived — a template's own lifecycle, independent of any card'sid_card_template_status enumTemplate routes
version (row)An optimistic-concurrency bigint, bumped by a database trigger on id_card_settings, id_card_templates, id_card_generation_jobs, and id_cards; sent back as a JSON number in every write DTO that carries oneschemaEvery write route on those four tables
verificationToken192 random bits, base64url, 32 characters — resolves a card on the scan and public-verify routes; never returned in a list responseid_cards.verification_tokenScan, public verification
existingPolicyskip/replace — what bulk generation does when a selected person already has a live cardid_card_existing_policy enumGeneration job creation
leaseTokenThe generation-runner's claim on a job row; only its holder may advance the job's statusid_card_generation_jobs.lease_tokenInternal to the worker; never exposed on the wire
snapshotThe frozen person/placement/school data a card was generated with — what the renderer reads, never live dataid_cards.snapshot (jsonb)Detail, render, download

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
AdminGET/api/id-card-templatesAdminJWT+RoleIdCardTemplates_READIdCardTemplatesControllerList templates
AdminGET/api/id-card-templates/field-catalogAdminJWT+RoleIdCardTemplates_READIdCardTemplatesControllerThe available design field keys per card type
AdminPOST/api/id-card-templatesAdminJWT+RoleIdCardTemplates_CREATEIdCardTemplatesControllerCreate a draft template
AdminGET/api/id-card-templates/:publicIdAdminJWT+RoleIdCardTemplates_READIdCardTemplatesControllerOne template
AdminPATCH/api/id-card-templates/:publicIdAdminJWT+RoleIdCardTemplates_UPDATEIdCardTemplatesControllerUpdate draft design/metadata
AdminGET/api/id-card-templates/:publicId/validationAdminJWT+RoleIdCardTemplates_READIdCardTemplatesControllerValidate the current draft design
AdminPOST/api/id-card-templates/:publicId/publishAdminJWT+RoleIdCardTemplates_UPDATEIdCardTemplatesControllerPublish an immutable version
AdminPOST/api/id-card-templates/:publicId/activateAdminJWT+RoleIdCardTemplates_UPDATEIdCardTemplatesControllerActivate
AdminPOST/api/id-card-templates/:publicId/deactivateAdminJWT+RoleIdCardTemplates_UPDATEIdCardTemplatesControllerDeactivate
AdminPOST/api/id-card-templates/:publicId/set-defaultAdminJWT+RoleIdCardTemplates_UPDATEIdCardTemplatesControllerMark as the default for its card type
AdminPOST/api/id-card-templates/:publicId/unset-defaultAdminJWT+RoleIdCardTemplates_UPDATEIdCardTemplatesControllerClear default status
AdminPOST/api/id-card-templates/:publicId/archiveAdminJWT+RoleIdCardTemplates_DELETEIdCardTemplatesControllerSoft-delete/archive
AdminPOST/api/id-card-templates/:publicId/unarchiveAdminJWT+RoleIdCardTemplates_RESTOREIdCardTemplatesControllerRestore an archived template
AdminDELETE/api/id-card-templates/:publicIdAdminJWT+RoleIdCardTemplates_DELETEIdCardTemplatesControllerDelete (never-published draft only)
AdminPOST/api/id-card-templates/:publicId/duplicateAdminJWT+RoleIdCardTemplates_CREATEIdCardTemplatesControllerDuplicate into a new draft
AdminGET/api/id-card-templates/:publicId/versionsAdminJWT+RoleIdCardTemplates_READIdCardTemplatesControllerList published versions
AdminGET/api/id-card-templates/:publicId/versions/:versionNoAdminJWT+RoleIdCardTemplates_READIdCardTemplatesControllerOne published version
AdminGET/api/id-card-templates/:publicId/usageAdminJWT+RoleIdCardTemplates_READIdCardTemplatesControllerCards referencing this template
AdminGET/api/id-card-template-assignmentsAdminJWT+RoleIdCardTemplates_READIdCardTemplateAssignmentsControllerList assignment rules
AdminPOST/api/id-card-template-assignmentsAdminJWT+RoleIdCardTemplates_UPDATEIdCardTemplateAssignmentsControllerCreate an assignment rule
AdminDELETE/api/id-card-template-assignments/:publicIdAdminJWT+RoleIdCardTemplates_UPDATEIdCardTemplateAssignmentsControllerRemove a rule
AdminGET/api/id-card-assetsAdminJWT+RoleIdCardTemplates_READIdCardAssetsControllerList assets
AdminPOST/api/id-card-assetsAdminJWT+RoleIdCardTemplates_UPDATEIdCardAssetsControllerUpload an asset
AdminGET/api/id-card-assets/:publicId/fileAdminJWT+RoleIdCards_READIdCardAssetsControllerStream the asset's bytes through the authenticated relay
AdminDELETE/api/id-card-assets/:publicIdAdminJWT+RoleIdCardTemplates_UPDATEIdCardAssetsControllerDelete an unused asset
AdminGET/api/id-card-settingsAdminJWT+RoleIdCardSettings_READIdCardSettingsControllerRead settings
AdminPATCH/api/id-card-settingsAdminJWT+RoleIdCardSettings_UPDATEIdCardSettingsControllerUpdate general settings
AdminPATCH/api/id-card-settings/public-verificationAdminJWT+RoleIdCardPublicVerification_UPDATEIdCardSettingsControllerUpdate public-page configuration (superadmin-only module)
AdminGET/api/id-card-generation/candidatesAdminJWT+RoleIdCards_CREATEIdCardGenerationControllerEligible people for a generation run
AdminGET/api/id-card-generation/peopleAdminJWT+RoleIdCards_READIdCardGenerationControllerPeople picker (search/browse)
AdminGET/api/id-card-generation/preview-dataAdminJWT+RoleIdCards_READIdCardGenerationControllerField values for a live design preview
AdminPOST/api/id-card-generation/cardsAdminJWT+RoleIdCards_CREATEIdCardGenerationControllerGenerate a single card synchronously
AdminPOST/api/id-card-generation-jobsAdminJWT+RoleIdCards_CREATEIdCardGenerationJobsControllerSubmit a bulk generation job
AdminGET/api/id-card-generation-jobsAdminJWT+RoleIdCards_READIdCardGenerationJobsControllerList jobs (row-scoped)
AdminGET/api/id-card-generation-jobs/:publicIdAdminJWT+RoleIdCards_READIdCardGenerationJobsControllerOne job (row-scoped, 404 outside scope)
AdminGET/api/id-card-generation-jobs/:publicId/itemsAdminJWT+RoleIdCards_READIdCardGenerationJobsControllerPer-person outcomes (row- and people-scoped)
AdminPOST/api/id-card-generation-jobs/:publicId/cancelAdminJWT+RoleIdCards_CREATEIdCardGenerationJobsControllerCancel a queued/running job (row-scoped)
AdminPOST/api/id-card-generation-jobs/:publicId/retryAdminJWT+RoleIdCards_CREATEIdCardGenerationJobsControllerRetry failed/skipped items (row-scoped)
AdminGET/api/id-cardsAdminJWT+RoleIdCards_READIdCardsControllerList cards
AdminGET/api/id-cards/dashboardAdminJWT+RoleIdCards_READIdCardsControllerAggregate counts by status/type
AdminGET/api/id-cards/replacementsAdminJWT+RoleIdCards_READIdCardsControllerReplacement chains
AdminGET/api/id-cards/historyAdminJWT+RoleIdCards_READIdCardsControllerCard event history
AdminGET/api/id-cards/:publicIdAdminJWT+RoleIdCards_READIdCardsControllerOne card's full detail, guardian block gated
AdminPOST/api/id-cards/renderAdminJWT+RoleIdCards_READIdCardActionsControllerRender a card image (preview/print)
AdminPOST/api/id-cards/print/mark-printedAdminJWT+RoleIdCardPrint_UPDATEIdCardActionsControllerMark a card printed
AdminPOST/api/id-cards/print/reprintAdminJWT+RoleIdCardPrint_UPDATEIdCardActionsControllerRecord a reprint
AdminPOST/api/id-cards/issueAdminJWT+RoleIdCardIssue_UPDATEIdCardActionsControllerHand a card to its holder (activates it)
AdminPOST/api/id-cards/:publicId/incidentsAdminJWT+RoleIdCardIncidents_UPDATEIdCardActionsControllerReport lost/stolen/damaged
AdminPOST/api/id-cards/:publicId/cancelAdminJWT+RoleIdCards_DELETEIdCardActionsControllerCancel a card
AdminPOST/api/id-cards/:publicId/replacementsAdminJWT+RoleIdCardReplace_CREATEIdCardActionsControllerGenerate a replacement
AdminPOST/api/id-card-scanAdminJWT+RoleIdCardVerify_READIdCardScanControllerAuthenticated scan by token or card number
PublicGET/api/id-card-verification/:tokenGuest@Public()N/AIdCardVerificationPublicControllerPublic verification page
PublicGET/api/id-card-verification/:token/photoGuest@Public()N/AIdCardVerificationPublicControllerPublic verification photo, when the school allows it

Note on id-cards route composition. IdCardsController and IdCardActionsController share the base path id-cards. Static segments (dashboard, replacements, history, render, print/mark-printed, print/reprint, issue) are literal children and never collide with the :publicId parameter route, because IdCardsController's own GET :publicId is the only bare-parameter route at that segment depth. Every :publicId param across the module is UUID-validated by the parameter pipe.

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
AdminJwtAuthGuard, RoleGuardreq.user.id, active role's granted permissionsIdCard*_ACTION per route (table above)NoNone of the 10 IdCard* modules is granted to the seeded staff/teacher/student/guardian roles — every reader is an explicit administrative grant, because a card carries a child's photo, DOB, and guardian contacts.
Admin (public verification)IdCardPublicVerification_UPDATESame as aboveSuperadmin-only module (listed in the module's restricted set)NoControls whether the OPEN internet can reach the public page and what it shows.
Publicnone (@Public())No identityN/AYesRate-limited only (PUBLIC_CARD_VERIFY, 60/min/IP). Response headers force no-store/noindex/no-referrer.

Row scoping beyond the permission check: every route returning a person applies the caller's people scope (derived from Students_READ/Staff_READ-style grants), and generation-job routes are additionally scoped to the requesting user unless that scope is unrestricted for the job's card type — see §13.1 and the backend doc §6.3.

6. DTO and Model Reference

6.1 CreateIdCardTemplateDto / UpdateIdCardTemplateDto

FieldTypeRequiredDefaultValidationSource
namestringYes (create)N/A1-120 chars, trimmed, unique per lowercase among live templatesid_card_templates.name
descriptionstringNonullmax 1000 chars
cardType"student" | "teacher" | "staff"Yes (create only — immutable after)N/Aenumid_card_type
academicSessionIdintegerNonullonly when cardType = "student"
orientation"portrait" | "landscape"YesN/Aenum
draftDesignobject (mm-based design document)YesN/Avalidated against the design schema (front/back sides, elements, dynamic field keys, safe margins)id-card-design.ts
versionnumberYes (update)N/Amust match the row's current versionOptimistic concurrency

6.2 GenerateIdCardDto (single card)

FieldTypeRequiredDefaultValidationSource
cardTypeenumYesN/Astudent/teacher/staff
personPublicIduuidYesN/AResolves a student or staff row
templateIdintegerNoresolved via assignment rules / defaultMust be active, matching cardType
academicSessionIdintegerConditionallyN/ARequired for cardType = "student"
validFrom/validUntildate (ISO)Nocomputed per studentValidity()/staffValidity()Both or neither; checkValidityOverride() rules apply when supplied

6.3 CreateIdCardGenerationJobDto

FieldTypeRequiredDefaultValidationSource
cardTypeenumYesN/A
personPublicIdsuuid[]YesN/A1-2000 entriesid_card_generation_jobs.total_count
templateIdintegerNoresolved per scope
academicSessionIdintegerConditionallyN/Arequired for student cards
existingPolicy"skip" | "replace"Yessettings' default_existing_policy if omitted
replacementReasonenumConditionallyN/Arequired when existingPolicy = "replace"; incident reasons (lost/stolen/damaged) refused
replacementNotestringConditionallyN/Arequired when replacementReason = "other", max 500 chars
validFrom/validUntildateNocomputedBoth or neither, ordered
clientRequestIduuidYesN/AIdempotency key — a repeat submission with the same value returns the original job instead of erroring (ID_CARD_JOB_DUPLICATE_REQUEST semantics)

6.4 ScanIdCardDto

FieldTypeRequiredDefaultValidationSource
codestringYesN/AEither a 32-char verification token or a card number; resolved by trying both shapesScanIdCardDto, id_cards.verification_token / card_number

6.5 ReportIdCardIncidentDto

FieldTypeRequiredDefaultValidationSource
reason"lost" | "stolen" | "damaged"YesN/AIncident-only subset of id_card_replacement_reason
notestringNonullmax 1000 chars (closed_reason)

6.6 IdCardResponseDto (detail)

FieldTypeNullableNotes
publicIduuidNo
cardNumberstringNo
cardTypeenumNo
statusenumNoStored lifecycle value.
verdictenumNoDerived live value — see §3.
personName, personCode, rollNumbermixedYes (roll number)Frozen at generation.
validFrom, validUntildateNo
snapshotobjectNoFull frozen design-field data. guardian key omitted entirely unless caller is superadmin or holds Guardians_READ.
generatedAt, printedAt, issuedAt, closedAttimestampYes (except generatedAt)
printCountintegerNo
replacesCardId/replacementReason/replacementNotemixedYesPopulated only for a replacement card.
versionnumberNo

Server-generated / read-only fields: publicId, cardNumber, verificationToken (never serialized to the response at all outside a render/download call), snapshot, generatedAt, printCount, version.

7. Enum Reference

EnumValueMeaningRuntime EffectSource
id_card_typestudentPupil cardRequires academicSessionId; placement fields (grade/section/class/roll) applyid-cards.ts
id_card_typeteacherTeaching staff cardSame shape as staff — a teacher is a staff row with a teaching designation
id_card_typestaffNon-teaching staff cardDepartment/designation fields apply; no session/placement
id_card_template_statusdraftEditable, never generated fromCannot generate; published_version_no must be NULL
id_card_template_statusactiveLive, usable for generation, may be defaultRequires a published version
id_card_template_statusinactivePublished but not currently offeredRequires a published version; cannot be default
id_card_template_statusarchivedSoft-deletedRefused if any card still references its versions
id_card_statusgeneratedCard exists, not yet printedLive; counts toward the one-pending-per-person limit
id_card_statusprintedPhysically printed, not yet issuedLive; still counts as pending
id_card_statusactiveIssued and in the holder's possessionLive; counts toward the one-active-per-person limit
id_card_statusexpiredPast validUntil, set by the lifecycle sweepTerminal
id_card_statuslost/stolen/damagedIncident-reportedTerminal, irreversible — the card can never return to active
id_card_statusreplacedSuperseded by a newer cardTerminal
id_card_statuscancelledAdministratively voidedTerminal
id_card_statusdeactivatedHolder left the school (or was soft-deleted)Terminal; set by the lifecycle sweep when auto_deactivate_on_leave is on, or immediately reflected in scan/verify verdicts even before the sweep runs
id_card_replacement_reasonlost/damaged/stolenIncident reasonsRefused on bulk-generation jobs; used only via the incident route
id_card_replacement_reasonname_change/photo_update/class_section_change/roll_number_change/designation_change/academic_year_renewal/template_change/otherNon-incident replacement reasonsUsable on both single replacement and bulk existingPolicy = "replace"
id_card_job_statusqueued/running/cancelling/completed/completed_with_errors/failed/cancelledJob lifecycleSee the backend doc's state diagram
id_card_job_item_statuspending/generated/skipped/failedPer-person outcome within a job
id_card_existing_policyskip/replaceBulk-generation behavior for a person who already has a live card
Verdict (derived, not an enum column)valid/not_yet_valid/expired/not_issued/replaced/lost/stolen/damaged/cancelled/deactivatedLive meaning shown to any readerComputed by cardVerdict() from status + datesid-card-rules.util.ts

8. Endpoint Reference

8.1 POST /api/id-card-generation-jobs

Purpose

Submit a bulk ID-card generation request for up to 2000 people at once. The admin panel calls this from the generation wizard after reviewing candidates and readiness. The job is processed asynchronously by the BullMQ worker; the response is the queued job, not the generated cards.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/id-cards/generation/admin/id-card-generation-jobs.controller.ts
DTOapps/api/src/modules/id-cards/generation/core/dto/*.ts
Serviceapps/api/src/modules/id-cards/generation/core/id-card-generation-jobs.service.ts
Schemapackages/db/src/schema/school/id-cards.ts (id_card_generation_jobs, id_card_generation_job_items)

Auth and Permissions

  • Auth: JWT
  • Guard chain: JwtAuthGuard, RoleGuard
  • Permission: IdCards_CREATE
  • Guest support: No
  • Rate limit: ID_CARD_JOB_SUBMIT — 20/hour per account
  • Idempotency: clientRequestId — a resubmission with the same value returns the original job

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>
BodyYesCreateIdCardGenerationJobDto — see §6.3
{
  "cardType": "student",
  "personPublicIds": ["b3f1...", "c2a0..."],
  "academicSessionId": 4,
  "existingPolicy": "skip",
  "clientRequestId": "8f6e2c10-..."
}

Response

{
  "success": true,
  "message": "Generation job queued.",
  "data": {
    "publicId": "1d2e...",
    "status": "queued",
    "totalCount": 2,
    "createdAt": "2026-09-17T05:12:00.000Z"
  }
}

Side Effects

  • Database writes: id_card_generation_jobs (insert), id_card_generation_job_items (one per person), an outbox_events row in the same transaction.
  • BullMQ: the outbox dispatcher relays IdCardJob.PROCESS_GENERATION_JOB onto QueueName.ID_CARDS.
  • Audit log: recorded automatically by ActivityAuditInterceptor under module IdCards, action CREATE.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
429ID_CARD_JOB_LIMIT_REACHEDCaller already has too many open jobsWait for an existing job to finishid-card-generation-jobs.service.ts
400ID_CARD_SELECTION_TOO_LARGEMore than 2000 people selectedSplit into multiple jobs
400ID_CARD_REPLACEMENT_REASON_INVALIDIncident reason used with existingPolicy: "replace"Use the incident route instead
400ID_CARD_REPLACEMENT_NOTE_REQUIREDReason other with no noteSupply a note

Edge Cases

  • Empty personPublicIds: rejected by DTO validation (400).
  • Resubmitting the same clientRequestId: returns the original job unchanged, no duplicate row created.
  • A person already having an active card under existingPolicy: "skip": that person's item is marked skipped when the job runs, not a request-level error.
  • Concurrent submissions from the same user: serialized by a per-user advisory lock so the open-job cap cannot be raced.

Example Requests

curl -X POST "$API_URL/api/id-card-generation-jobs" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"cardType":"student","personPublicIds":["b3f1..."],"academicSessionId":4,"existingPolicy":"skip","clientRequestId":"8f6e2c10-..."}'

8.2 GET /api/id-card-generation-jobs/:publicId

Purpose

Read one generation job's status and progress. Used by the admin panel to poll a submitted job.

Auth and Permissions

  • Permission: IdCards_READ
  • Row scope: the caller sees this job only if they requested it, or their people-scope for the job's card type is unrestricted ({kind:"all"}). Otherwise 404.

Response

{
  "success": true,
  "message": "Job fetched.",
  "data": {
    "publicId": "1d2e...",
    "status": "completed_with_errors",
    "totalCount": 2,
    "startedAt": "2026-09-17T05:12:05.000Z",
    "finishedAt": "2026-09-17T05:12:11.000Z"
  }
}

Error Cases

HTTP StatusError CodeConditionUser-Facing Meaning
404ID_CARD_JOB_NOT_FOUNDJob does not exist, OR exists outside the caller's row scopeRe-check the id; deliberately indistinguishable from "does not exist" to avoid an enumeration oracle

8.3 GET /api/id-card-generation-jobs/:publicId/items

Purpose

List the per-person outcomes within a job — which cards generated, which were skipped, and why.

Auth and Permissions

  • Permission: IdCards_READ
  • Row scope: same as §8.2 for the parent job, PLUS each returned item is filtered by the caller's own people scope — a scoped caller may see fewer items than the job's totalCount even when the job itself is visible.

Response

{
  "success": true,
  "message": "Items fetched.",
  "data": {
    "items": [
      { "studentId": "...", "status": "generated", "cardId": "...", "warnings": [] },
      { "studentId": "...", "status": "skipped", "outcomeCode": "ID_CARD_ACTIVE_CARD_EXISTS", "warnings": [] }
    ]
  }
}

8.4 POST /api/id-card-generation-jobs/:publicId/cancel

Purpose

Stop a queued or running bulk job.

Auth and Permissions

  • Permission: IdCards_CREATE
  • Row scope: same as §8.2.

Side Effects

  • queued → cancelled immediately, pending items marked skipped.
  • running → cancelling; the worker observes the flag between items and finishes the transition to cancelled.
  • The job row is locked FOR UPDATE before the transition decision, so a concurrent cancel/retry or the worker's own claim serializes on the lock.

Error Cases

HTTP StatusError CodeCondition
409ID_CARD_JOB_INVALID_TRANSITIONJob is already terminal (completed/completed_with_errors/failed/cancelled)

8.5 POST /api/id-card-generation-jobs/:publicId/retry

Purpose

Re-run the failed/skipped items of a job that finished with errors.

Auth and Permissions

  • Permission: IdCards_CREATE
  • Row scope: same as §8.2.

Error Cases

HTTP StatusError CodeCondition
409ID_CARD_JOB_NOTHING_TO_RETRYNo failed/skipped items exist
409ID_CARD_JOB_INVALID_TRANSITIONJob is cancelled or cancelling — refused explicitly rather than reported as "nothing to retry"

8.6 GET /api/id-cards/:publicId

Purpose

Full detail of one card, for the admin panel's card view.

Auth and Permissions

  • Permission: IdCards_READ
  • People scope applies to the underlying person.

Response

{
  "success": true,
  "message": "Card fetched.",
  "data": {
    "publicId": "9a1c...",
    "cardNumber": "STU-000041",
    "status": "active",
    "verdict": "valid",
    "personName": "Aarav Sharma",
    "snapshot": { "photo": "...", "class": "Grade 5-A" },
    "validFrom": "2026-04-01",
    "validUntil": "2027-03-31"
  }
}

Edge Cases

  • Caller lacks Guardians_READ and is not superadmin: snapshot.guardian is absent from the object entirely (not null), so the response shape itself signals nothing about whether guardian data exists.
  • Card's validUntil has passed but the sweep has not yet run: status still reads active in the raw column, but verdict reads expired — clients should always render verdict, never status, for "is this card currently valid".

8.7 POST /api/id-cards/issue

Purpose

Hand a printed card to its holder. This is the action that moves a card into active status and, if the holder had a prior active card, closes it in the same transaction.

Auth and Permissions

  • Permission: IdCardIssue_UPDATE

Error Cases

HTTP StatusError CodeCondition
409ID_CARD_INVALID_TRANSITIONCard is not in generated/printed (or allow_issue_without_print is off and the card was never printed)
409ID_CARD_NOT_YET_VALIDThe card's validFrom is after the school's current date — a card cannot be issued before its own validity window begins

Edge Cases

  • Issuing while the holder still has an active card: the prior card is closed (status transitions away from active) in the SAME transaction as the new card's activation, because the database's partial unique index on active cards is not deferrable and would otherwise refuse the insert-then-close ordering.

8.8 POST /api/id-card-scan

Purpose

The authenticated scan a gate or front-office operator performs — resolves a card by its QR token or its printed card number and returns enough detail to identify the holder.

Auth and Permissions

  • Permission: IdCardVerify_READ
  • Rate limit: ID_CARD_SCAN — 120/min per account

Request

{ "code": "K3f9pQ2..." }

Response

Full card detail when scanned by token; basic info only when looked up by card number. Guardian contact details are included only when the caller also holds Guardians_READ.

{
  "success": true,
  "message": "Card scanned.",
  "data": {
    "verdict": "valid",
    "personName": "Aarav Sharma",
    "cardNumber": "STU-000041",
    "guardians": [{ "name": "...", "phone": "..." }]
  }
}

Error Cases

HTTP StatusError CodeCondition
400ID_CARD_SCAN_CODE_INVALIDcode matches neither a token nor a card number shape

Edge Cases

  • Holder has left the school or is soft-deleted: verdict reads deactivated immediately, even before the hourly lifecycle sweep records it on the card row.
  • Card-number lookup (as opposed to token) never returns guardian contacts or full detail, regardless of the caller's permissions — the full-detail path is reserved for an actual QR scan.

8.9 GET /api/id-card-verification/:token

Purpose

The open-internet page reached by scanning a printed card's QR code. Shows the minimum information the school has configured under publicShow* settings.

Auth and Permissions

  • Auth: none (@Public())
  • Rate limit: PUBLIC_CARD_VERIFY — 60/min per IP

Response

{
  "success": true,
  "message": "Card verified.",
  "data": {
    "verdict": "valid",
    "name": "A. Sharma",
    "cardNumber": "STU-000041"
  }
}

Response headers on every call: Cache-Control: no-store, X-Robots-Tag: noindex, Referrer-Policy: no-referrer.

Error Cases

HTTP StatusError CodeCondition
409ID_CARD_VERIFICATION_DISABLEDpublic_verification_enabled is off
404ID_CARD_NOT_FOUNDToken does not resolve to any card

Edge Cases

  • Deactivated/expired holder: verdict reflects it immediately, same as the authenticated scan.
  • publicShowName: "hidden": name field is omitted or empty per settings, never guessed at.

8.10 GET /api/id-card-verification/:token/photo

Purpose

The holder's photo for the public verification page, when the school has enabled publicShowPhoto.

Auth and Permissions

  • Auth: none (@Public())
  • Rate limit: same bucket as :token (class-level PUBLIC_CARD_VERIFY)

Response

Raw image bytes with the correct Content-Type, not the JSON envelope.

Error Cases

HTTP StatusError CodeCondition
409ID_CARD_VERIFICATION_DISABLEDpublicShowPhoto is off, or public verification is disabled entirely
404ID_CARD_NOT_FOUNDToken does not resolve

9. Flow Diagrams

9.1 Route Ownership

9.2 Bulk Generation Request Sequence

9.3 Error Branch — Scan

EndpointPagination TypeDefault SizeMax SizeSort FieldsFiltersResult Cap
GET /api/id-cardspage/pageSize (PaginationUtil)20100generatedAt, personNamestatus, cardType, academicSessionId, gradeId, sectionId, search by name/card numberPaginationUtil.MAX_PAGE_SIZE
GET /api/id-card-generation-jobspage/pageSize20100createdAtstatus, cardTypeSame
GET /api/id-card-templatespage/pageSize20100name, updatedAtcardType, statusSame
GET /api/id-card-assetspage/pageSize20100createdAtnoneSame
GET /api/id-cards/historypage/pageSize20100createdAteventType, cardIdSame

None of the module's list endpoints allow pagination=false — cards and generation jobs are person-scale data, matching the repo-wide rule that the people tables refuse unpaginated rolls.

11. Caching, Jobs, and External Integrations

IntegrationUsed?DetailsSource
Redis cacheNoThe module keeps no Redis-backed cache of its own—
BullMQYesQueueName.ID_CARDS; PROCESS_GENERATION_JOB, RECOVERY_SWEEP, LIFECYCLE_SWEEPpackages/jobs/src/index.ts, workers/id-cards-queue.processor.ts
External APINo—
File storageYesPrivate ID_CARD_ASSET upload type; served only through GET /api/id-card-assets/:publicId/filepackages/storage

12. Zero-Omission API Checklist

  • Every controller route is documented (10 controllers, 46 routes).
  • Every parent route prefix is documented.
  • Every DTO's principal fields, enum, default, and validator are documented at the level the schema/service enforces them.
  • Every response field's nullability and gating is documented for detail/scan/public-verify.
  • Every auth, guard, permission, and guest branch is documented.
  • Every success, validation, permission, not-found, and conflict branch is documented for the ten endpoints given full detail; the remaining routes are covered in the surface map and error-code table.
  • Database writes, BullMQ jobs, and external calls (storage relay) are documented.
  • Route ownership, sequence, and error-branch diagrams are included.
  • Links to backend and features/flows docs are present.

Integration Checklist

  • Every route from controllers is documented.
  • Every enum value is documented.
  • Every response envelope follows { success, message, data }.
  • Every error code relevant to the module is documented (see the backend doc's full table).
  • Every auth guard and permission is documented.
  • Every queue job and external call (storage) is documented.
  • Diagrams match the current controllers and services.

See Also

On this page

ID Cards - API Reference1. Documentation Evidence2. Module Summary3. Concepts and Terminology4. API Surface Map5. Auth, Identity, and Permissions6. DTO and Model Reference6.1 CreateIdCardTemplateDto / UpdateIdCardTemplateDto6.2 GenerateIdCardDto (single card)6.3 CreateIdCardGenerationJobDto6.4 ScanIdCardDto6.5 ReportIdCardIncidentDto6.6 IdCardResponseDto (detail)7. Enum Reference8. Endpoint Reference8.1 POST /api/id-card-generation-jobsPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.2 GET /api/id-card-generation-jobs/:publicIdPurposeAuth and PermissionsResponseError Cases8.3 GET /api/id-card-generation-jobs/:publicId/itemsPurposeAuth and PermissionsResponse8.4 POST /api/id-card-generation-jobs/:publicId/cancelPurposeAuth and PermissionsSide EffectsError Cases8.5 POST /api/id-card-generation-jobs/:publicId/retryPurposeAuth and PermissionsError Cases8.6 GET /api/id-cards/:publicIdPurposeAuth and PermissionsResponseEdge Cases8.7 POST /api/id-cards/issuePurposeAuth and PermissionsError CasesEdge Cases8.8 POST /api/id-card-scanPurposeAuth and PermissionsRequestResponseError CasesEdge Cases8.9 GET /api/id-card-verification/:tokenPurposeAuth and PermissionsResponseError CasesEdge Cases8.10 GET /api/id-card-verification/:token/photoPurposeAuth and PermissionsResponseError Cases9. Flow Diagrams9.1 Route Ownership9.2 Bulk Generation Request Sequence9.3 Error Branch — Scan10. Pagination, Sorting, Filtering, and Search11. Caching, Jobs, and External Integrations12. Zero-Omission API ChecklistIntegration ChecklistSee Also