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
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | apps/api/src/modules/id-cards/**/*.controller.ts (10 files) | Routes, methods, guards, decorators, permissions |
| DTOs | apps/api/src/modules/id-cards/**/dto/*.ts | Request, query, response shapes |
| Services | apps/api/src/modules/id-cards/**/*.service.ts | Behavior, side effects, errors |
| Schema | packages/db/src/schema/school/id-cards.ts | IDs, enums, persisted fields, constraints |
| Error registry | apps/api/src/common/types/error-codes.ts (ID_CARD_*) | 46 error codes |
| Rate limits | apps/api/src/common/guards/ip-throttler.config.ts | PUBLIC_CARD_VERIFY, ID_CARD_SCAN, ID_CARD_JOB_SUBMIT |
| Permissions | packages/db/src/authorization/permission-catalog.ts | 10 IdCard* modules |
| Module wiring | apps/api/src/app.module.ts, apps/api/src/config/swagger/swagger-documents.ts | Membership in ADMIN_MODULES |
2. Module Summary
| Field | Value |
|---|---|
| Module name | IdCards |
| Module slug | id-cards |
| Primary actors | admin (school staff with granted permissions), guest (the public verification page only) |
| API surfaces | Admin (/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 model | JwtAuthGuard (global) + RoleGuard with @Permissions("Module_ACTION") on every admin route; the public verification controller carries @Public() and no guard beyond the rate limiter |
| Persistence | PostgreSQL (Drizzle), BullMQ (@skoolsewa/jobs, QueueName.ID_CARDS), file bytes via @skoolsewa/storage |
| Runtime source of truth | id_cards.status (stored); the live verdict shown to any caller is derived from status + validity dates against the school's current date |
| Sibling docs | Backend, Features and flows |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
publicId | External UUID for a card, template, template version, assignment, asset, or generation job — never the internal PK | id-cards.ts schema | Every route param named publicId |
status | The card lifecycle: generated/printed/active/expired/lost/stolen/damaged/replaced/cancelled/deactivated | id_cards.status | List/detail responses, every lifecycle route |
verdict | The 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 stored | cardVerdict() in id-card-rules.util.ts | Detail, scan, and public-verify responses |
cardType | student/teacher/staff — fixes a template's field catalogue and a card's placement columns | id_card_type enum | Templates, generation, cards |
templateStatus | draft/active/inactive/archived — a template's own lifecycle, independent of any card's | id_card_template_status enum | Template 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 one | schema | Every write route on those four tables |
verificationToken | 192 random bits, base64url, 32 characters — resolves a card on the scan and public-verify routes; never returned in a list response | id_cards.verification_token | Scan, public verification |
existingPolicy | skip/replace — what bulk generation does when a selected person already has a live card | id_card_existing_policy enum | Generation job creation |
leaseToken | The generation-runner's claim on a job row; only its holder may advance the job's status | id_card_generation_jobs.lease_token | Internal to the worker; never exposed on the wire |
snapshot | The frozen person/placement/school data a card was generated with — what the renderer reads, never live data | id_cards.snapshot (jsonb) | Detail, render, download |
4. API Surface Map
| Surface | Method | Path | Actor | Auth/Guard | Permission | Controller | Purpose |
|---|---|---|---|---|---|---|---|
| Admin | GET | /api/id-card-templates | Admin | JWT+Role | IdCardTemplates_READ | IdCardTemplatesController | List templates |
| Admin | GET | /api/id-card-templates/field-catalog | Admin | JWT+Role | IdCardTemplates_READ | IdCardTemplatesController | The available design field keys per card type |
| Admin | POST | /api/id-card-templates | Admin | JWT+Role | IdCardTemplates_CREATE | IdCardTemplatesController | Create a draft template |
| Admin | GET | /api/id-card-templates/:publicId | Admin | JWT+Role | IdCardTemplates_READ | IdCardTemplatesController | One template |
| Admin | PATCH | /api/id-card-templates/:publicId | Admin | JWT+Role | IdCardTemplates_UPDATE | IdCardTemplatesController | Update draft design/metadata |
| Admin | GET | /api/id-card-templates/:publicId/validation | Admin | JWT+Role | IdCardTemplates_READ | IdCardTemplatesController | Validate the current draft design |
| Admin | POST | /api/id-card-templates/:publicId/publish | Admin | JWT+Role | IdCardTemplates_UPDATE | IdCardTemplatesController | Publish an immutable version |
| Admin | POST | /api/id-card-templates/:publicId/activate | Admin | JWT+Role | IdCardTemplates_UPDATE | IdCardTemplatesController | Activate |
| Admin | POST | /api/id-card-templates/:publicId/deactivate | Admin | JWT+Role | IdCardTemplates_UPDATE | IdCardTemplatesController | Deactivate |
| Admin | POST | /api/id-card-templates/:publicId/set-default | Admin | JWT+Role | IdCardTemplates_UPDATE | IdCardTemplatesController | Mark as the default for its card type |
| Admin | POST | /api/id-card-templates/:publicId/unset-default | Admin | JWT+Role | IdCardTemplates_UPDATE | IdCardTemplatesController | Clear default status |
| Admin | POST | /api/id-card-templates/:publicId/archive | Admin | JWT+Role | IdCardTemplates_DELETE | IdCardTemplatesController | Soft-delete/archive |
| Admin | POST | /api/id-card-templates/:publicId/unarchive | Admin | JWT+Role | IdCardTemplates_RESTORE | IdCardTemplatesController | Restore an archived template |
| Admin | DELETE | /api/id-card-templates/:publicId | Admin | JWT+Role | IdCardTemplates_DELETE | IdCardTemplatesController | Delete (never-published draft only) |
| Admin | POST | /api/id-card-templates/:publicId/duplicate | Admin | JWT+Role | IdCardTemplates_CREATE | IdCardTemplatesController | Duplicate into a new draft |
| Admin | GET | /api/id-card-templates/:publicId/versions | Admin | JWT+Role | IdCardTemplates_READ | IdCardTemplatesController | List published versions |
| Admin | GET | /api/id-card-templates/:publicId/versions/:versionNo | Admin | JWT+Role | IdCardTemplates_READ | IdCardTemplatesController | One published version |
| Admin | GET | /api/id-card-templates/:publicId/usage | Admin | JWT+Role | IdCardTemplates_READ | IdCardTemplatesController | Cards referencing this template |
| Admin | GET | /api/id-card-template-assignments | Admin | JWT+Role | IdCardTemplates_READ | IdCardTemplateAssignmentsController | List assignment rules |
| Admin | POST | /api/id-card-template-assignments | Admin | JWT+Role | IdCardTemplates_UPDATE | IdCardTemplateAssignmentsController | Create an assignment rule |
| Admin | DELETE | /api/id-card-template-assignments/:publicId | Admin | JWT+Role | IdCardTemplates_UPDATE | IdCardTemplateAssignmentsController | Remove a rule |
| Admin | GET | /api/id-card-assets | Admin | JWT+Role | IdCardTemplates_READ | IdCardAssetsController | List assets |
| Admin | POST | /api/id-card-assets | Admin | JWT+Role | IdCardTemplates_UPDATE | IdCardAssetsController | Upload an asset |
| Admin | GET | /api/id-card-assets/:publicId/file | Admin | JWT+Role | IdCards_READ | IdCardAssetsController | Stream the asset's bytes through the authenticated relay |
| Admin | DELETE | /api/id-card-assets/:publicId | Admin | JWT+Role | IdCardTemplates_UPDATE | IdCardAssetsController | Delete an unused asset |
| Admin | GET | /api/id-card-settings | Admin | JWT+Role | IdCardSettings_READ | IdCardSettingsController | Read settings |
| Admin | PATCH | /api/id-card-settings | Admin | JWT+Role | IdCardSettings_UPDATE | IdCardSettingsController | Update general settings |
| Admin | PATCH | /api/id-card-settings/public-verification | Admin | JWT+Role | IdCardPublicVerification_UPDATE | IdCardSettingsController | Update public-page configuration (superadmin-only module) |
| Admin | GET | /api/id-card-generation/candidates | Admin | JWT+Role | IdCards_CREATE | IdCardGenerationController | Eligible people for a generation run |
| Admin | GET | /api/id-card-generation/people | Admin | JWT+Role | IdCards_READ | IdCardGenerationController | People picker (search/browse) |
| Admin | GET | /api/id-card-generation/preview-data | Admin | JWT+Role | IdCards_READ | IdCardGenerationController | Field values for a live design preview |
| Admin | POST | /api/id-card-generation/cards | Admin | JWT+Role | IdCards_CREATE | IdCardGenerationController | Generate a single card synchronously |
| Admin | POST | /api/id-card-generation-jobs | Admin | JWT+Role | IdCards_CREATE | IdCardGenerationJobsController | Submit a bulk generation job |
| Admin | GET | /api/id-card-generation-jobs | Admin | JWT+Role | IdCards_READ | IdCardGenerationJobsController | List jobs (row-scoped) |
| Admin | GET | /api/id-card-generation-jobs/:publicId | Admin | JWT+Role | IdCards_READ | IdCardGenerationJobsController | One job (row-scoped, 404 outside scope) |
| Admin | GET | /api/id-card-generation-jobs/:publicId/items | Admin | JWT+Role | IdCards_READ | IdCardGenerationJobsController | Per-person outcomes (row- and people-scoped) |
| Admin | POST | /api/id-card-generation-jobs/:publicId/cancel | Admin | JWT+Role | IdCards_CREATE | IdCardGenerationJobsController | Cancel a queued/running job (row-scoped) |
| Admin | POST | /api/id-card-generation-jobs/:publicId/retry | Admin | JWT+Role | IdCards_CREATE | IdCardGenerationJobsController | Retry failed/skipped items (row-scoped) |
| Admin | GET | /api/id-cards | Admin | JWT+Role | IdCards_READ | IdCardsController | List cards |
| Admin | GET | /api/id-cards/dashboard | Admin | JWT+Role | IdCards_READ | IdCardsController | Aggregate counts by status/type |
| Admin | GET | /api/id-cards/replacements | Admin | JWT+Role | IdCards_READ | IdCardsController | Replacement chains |
| Admin | GET | /api/id-cards/history | Admin | JWT+Role | IdCards_READ | IdCardsController | Card event history |
| Admin | GET | /api/id-cards/:publicId | Admin | JWT+Role | IdCards_READ | IdCardsController | One card's full detail, guardian block gated |
| Admin | POST | /api/id-cards/render | Admin | JWT+Role | IdCards_READ | IdCardActionsController | Render a card image (preview/print) |
| Admin | POST | /api/id-cards/print/mark-printed | Admin | JWT+Role | IdCardPrint_UPDATE | IdCardActionsController | Mark a card printed |
| Admin | POST | /api/id-cards/print/reprint | Admin | JWT+Role | IdCardPrint_UPDATE | IdCardActionsController | Record a reprint |
| Admin | POST | /api/id-cards/issue | Admin | JWT+Role | IdCardIssue_UPDATE | IdCardActionsController | Hand a card to its holder (activates it) |
| Admin | POST | /api/id-cards/:publicId/incidents | Admin | JWT+Role | IdCardIncidents_UPDATE | IdCardActionsController | Report lost/stolen/damaged |
| Admin | POST | /api/id-cards/:publicId/cancel | Admin | JWT+Role | IdCards_DELETE | IdCardActionsController | Cancel a card |
| Admin | POST | /api/id-cards/:publicId/replacements | Admin | JWT+Role | IdCardReplace_CREATE | IdCardActionsController | Generate a replacement |
| Admin | POST | /api/id-card-scan | Admin | JWT+Role | IdCardVerify_READ | IdCardScanController | Authenticated scan by token or card number |
| Public | GET | /api/id-card-verification/:token | Guest | @Public() | N/A | IdCardVerificationPublicController | Public verification page |
| Public | GET | /api/id-card-verification/:token/photo | Guest | @Public() | N/A | IdCardVerificationPublicController | Public 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
| Surface | Guard/Decorator | Identity Shape | Permission | Guest Allowed | Notes |
|---|---|---|---|---|---|
| Admin | JwtAuthGuard, RoleGuard | req.user.id, active role's granted permissions | IdCard*_ACTION per route (table above) | No | None 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_UPDATE | Same as above | Superadmin-only module (listed in the module's restricted set) | No | Controls whether the OPEN internet can reach the public page and what it shows. |
| Public | none (@Public()) | No identity | N/A | Yes | Rate-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
| Field | Type | Required | Default | Validation | Source |
|---|---|---|---|---|---|
name | string | Yes (create) | N/A | 1-120 chars, trimmed, unique per lowercase among live templates | id_card_templates.name |
description | string | No | null | max 1000 chars | |
cardType | "student" | "teacher" | "staff" | Yes (create only — immutable after) | N/A | enum | id_card_type |
academicSessionId | integer | No | null | only when cardType = "student" | |
orientation | "portrait" | "landscape" | Yes | N/A | enum | |
draftDesign | object (mm-based design document) | Yes | N/A | validated against the design schema (front/back sides, elements, dynamic field keys, safe margins) | id-card-design.ts |
version | number | Yes (update) | N/A | must match the row's current version | Optimistic concurrency |
6.2 GenerateIdCardDto (single card)
| Field | Type | Required | Default | Validation | Source |
|---|---|---|---|---|---|
cardType | enum | Yes | N/A | student/teacher/staff | |
personPublicId | uuid | Yes | N/A | Resolves a student or staff row | |
templateId | integer | No | resolved via assignment rules / default | Must be active, matching cardType | |
academicSessionId | integer | Conditionally | N/A | Required for cardType = "student" | |
validFrom/validUntil | date (ISO) | No | computed per studentValidity()/staffValidity() | Both or neither; checkValidityOverride() rules apply when supplied |
6.3 CreateIdCardGenerationJobDto
| Field | Type | Required | Default | Validation | Source |
|---|---|---|---|---|---|
cardType | enum | Yes | N/A | ||
personPublicIds | uuid[] | Yes | N/A | 1-2000 entries | id_card_generation_jobs.total_count |
templateId | integer | No | resolved per scope | ||
academicSessionId | integer | Conditionally | N/A | required for student cards | |
existingPolicy | "skip" | "replace" | Yes | settings' default_existing_policy if omitted | ||
replacementReason | enum | Conditionally | N/A | required when existingPolicy = "replace"; incident reasons (lost/stolen/damaged) refused | |
replacementNote | string | Conditionally | N/A | required when replacementReason = "other", max 500 chars | |
validFrom/validUntil | date | No | computed | Both or neither, ordered | |
clientRequestId | uuid | Yes | N/A | Idempotency key — a repeat submission with the same value returns the original job instead of erroring (ID_CARD_JOB_DUPLICATE_REQUEST semantics) |
6.4 ScanIdCardDto
| Field | Type | Required | Default | Validation | Source |
|---|---|---|---|---|---|
code | string | Yes | N/A | Either a 32-char verification token or a card number; resolved by trying both shapes | ScanIdCardDto, id_cards.verification_token / card_number |
6.5 ReportIdCardIncidentDto
| Field | Type | Required | Default | Validation | Source |
|---|---|---|---|---|---|
reason | "lost" | "stolen" | "damaged" | Yes | N/A | Incident-only subset of id_card_replacement_reason | |
note | string | No | null | max 1000 chars (closed_reason) |
6.6 IdCardResponseDto (detail)
| Field | Type | Nullable | Notes |
|---|---|---|---|
publicId | uuid | No | |
cardNumber | string | No | |
cardType | enum | No | |
status | enum | No | Stored lifecycle value. |
verdict | enum | No | Derived live value — see §3. |
personName, personCode, rollNumber | mixed | Yes (roll number) | Frozen at generation. |
validFrom, validUntil | date | No | |
snapshot | object | No | Full frozen design-field data. guardian key omitted entirely unless caller is superadmin or holds Guardians_READ. |
generatedAt, printedAt, issuedAt, closedAt | timestamp | Yes (except generatedAt) | |
printCount | integer | No | |
replacesCardId/replacementReason/replacementNote | mixed | Yes | Populated only for a replacement card. |
version | number | No |
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
| Enum | Value | Meaning | Runtime Effect | Source |
|---|---|---|---|---|
id_card_type | student | Pupil card | Requires academicSessionId; placement fields (grade/section/class/roll) apply | id-cards.ts |
id_card_type | teacher | Teaching staff card | Same shape as staff — a teacher is a staff row with a teaching designation | |
id_card_type | staff | Non-teaching staff card | Department/designation fields apply; no session/placement | |
id_card_template_status | draft | Editable, never generated from | Cannot generate; published_version_no must be NULL | |
id_card_template_status | active | Live, usable for generation, may be default | Requires a published version | |
id_card_template_status | inactive | Published but not currently offered | Requires a published version; cannot be default | |
id_card_template_status | archived | Soft-deleted | Refused if any card still references its versions | |
id_card_status | generated | Card exists, not yet printed | Live; counts toward the one-pending-per-person limit | |
id_card_status | printed | Physically printed, not yet issued | Live; still counts as pending | |
id_card_status | active | Issued and in the holder's possession | Live; counts toward the one-active-per-person limit | |
id_card_status | expired | Past validUntil, set by the lifecycle sweep | Terminal | |
id_card_status | lost/stolen/damaged | Incident-reported | Terminal, irreversible — the card can never return to active | |
id_card_status | replaced | Superseded by a newer card | Terminal | |
id_card_status | cancelled | Administratively voided | Terminal | |
id_card_status | deactivated | Holder 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_reason | lost/damaged/stolen | Incident reasons | Refused on bulk-generation jobs; used only via the incident route | |
id_card_replacement_reason | name_change/photo_update/class_section_change/roll_number_change/designation_change/academic_year_renewal/template_change/other | Non-incident replacement reasons | Usable on both single replacement and bulk existingPolicy = "replace" | |
id_card_job_status | queued/running/cancelling/completed/completed_with_errors/failed/cancelled | Job lifecycle | See the backend doc's state diagram | |
id_card_job_item_status | pending/generated/skipped/failed | Per-person outcome within a job | ||
id_card_existing_policy | skip/replace | Bulk-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/deactivated | Live meaning shown to any reader | Computed by cardVerdict() from status + dates | id-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
| Evidence | Path |
|---|---|
| Controller | apps/api/src/modules/id-cards/generation/admin/id-card-generation-jobs.controller.ts |
| DTO | apps/api/src/modules/id-cards/generation/core/dto/*.ts |
| Service | apps/api/src/modules/id-cards/generation/core/id-card-generation-jobs.service.ts |
| Schema | packages/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
| Part | Required | Details |
|---|---|---|
| Headers | Yes | Authorization: Bearer <token> |
| Body | Yes | CreateIdCardGenerationJobDto — 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), anoutbox_eventsrow in the same transaction. - BullMQ: the outbox dispatcher relays
IdCardJob.PROCESS_GENERATION_JOBontoQueueName.ID_CARDS. - Audit log: recorded automatically by
ActivityAuditInterceptorunder moduleIdCards, actionCREATE.
Error Cases
| HTTP Status | Error Code | Condition | User-Facing Meaning | Source |
|---|---|---|---|---|
429 | ID_CARD_JOB_LIMIT_REACHED | Caller already has too many open jobs | Wait for an existing job to finish | id-card-generation-jobs.service.ts |
400 | ID_CARD_SELECTION_TOO_LARGE | More than 2000 people selected | Split into multiple jobs | |
400 | ID_CARD_REPLACEMENT_REASON_INVALID | Incident reason used with existingPolicy: "replace" | Use the incident route instead | |
400 | ID_CARD_REPLACEMENT_NOTE_REQUIRED | Reason other with no note | Supply 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 markedskippedwhen 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"}). Otherwise404.
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 Status | Error Code | Condition | User-Facing Meaning |
|---|---|---|---|
404 | ID_CARD_JOB_NOT_FOUND | Job does not exist, OR exists outside the caller's row scope | Re-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
totalCounteven 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→cancelledimmediately, pending items markedskipped.running→cancelling; the worker observes the flag between items and finishes the transition tocancelled.- The job row is locked
FOR UPDATEbefore the transition decision, so a concurrent cancel/retry or the worker's own claim serializes on the lock.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
409 | ID_CARD_JOB_INVALID_TRANSITION | Job 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 Status | Error Code | Condition |
|---|---|---|
409 | ID_CARD_JOB_NOTHING_TO_RETRY | No failed/skipped items exist |
409 | ID_CARD_JOB_INVALID_TRANSITION | Job 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_READand is not superadmin:snapshot.guardianis absent from the object entirely (notnull), so the response shape itself signals nothing about whether guardian data exists. - Card's
validUntilhas passed but the sweep has not yet run:statusstill readsactivein the raw column, butverdictreadsexpired— clients should always renderverdict, neverstatus, 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 Status | Error Code | Condition |
|---|---|---|
409 | ID_CARD_INVALID_TRANSITION | Card is not in generated/printed (or allow_issue_without_print is off and the card was never printed) |
409 | ID_CARD_NOT_YET_VALID | The 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 onactivecards 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 Status | Error Code | Condition |
|---|---|---|
400 | ID_CARD_SCAN_CODE_INVALID | code matches neither a token nor a card number shape |
Edge Cases
- Holder has left the school or is soft-deleted: verdict reads
deactivatedimmediately, 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 Status | Error Code | Condition |
|---|---|---|
409 | ID_CARD_VERIFICATION_DISABLED | public_verification_enabled is off |
404 | ID_CARD_NOT_FOUND | Token does not resolve to any card |
Edge Cases
- Deactivated/expired holder: verdict reflects it immediately, same as the authenticated scan.
publicShowName: "hidden":namefield 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-levelPUBLIC_CARD_VERIFY)
Response
Raw image bytes with the correct Content-Type, not the JSON envelope.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
409 | ID_CARD_VERIFICATION_DISABLED | publicShowPhoto is off, or public verification is disabled entirely |
404 | ID_CARD_NOT_FOUND | Token does not resolve |
9. Flow Diagrams
9.1 Route Ownership
9.2 Bulk Generation Request Sequence
9.3 Error Branch — Scan
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination Type | Default Size | Max Size | Sort Fields | Filters | Result Cap |
|---|---|---|---|---|---|---|
GET /api/id-cards | page/pageSize (PaginationUtil) | 20 | 100 | generatedAt, personName | status, cardType, academicSessionId, gradeId, sectionId, search by name/card number | PaginationUtil.MAX_PAGE_SIZE |
GET /api/id-card-generation-jobs | page/pageSize | 20 | 100 | createdAt | status, cardType | Same |
GET /api/id-card-templates | page/pageSize | 20 | 100 | name, updatedAt | cardType, status | Same |
GET /api/id-card-assets | page/pageSize | 20 | 100 | createdAt | none | Same |
GET /api/id-cards/history | page/pageSize | 20 | 100 | createdAt | eventType, cardId | Same |
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
| Integration | Used? | Details | Source |
|---|---|---|---|
| Redis cache | No | The module keeps no Redis-backed cache of its own | — |
| BullMQ | Yes | QueueName.ID_CARDS; PROCESS_GENERATION_JOB, RECOVERY_SWEEP, LIFECYCLE_SWEEP | packages/jobs/src/index.ts, workers/id-cards-queue.processor.ts |
| External API | No | — | |
| File storage | Yes | Private ID_CARD_ASSET upload type; served only through GET /api/id-card-assets/:publicId/file | packages/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
- Backend doc:
/docs/developer/id-cards/backend - Features and flows doc:
/docs/developer/id-cards/feature