Skip to main content

Bulk import files

The bulk import page (/records/import) reads a records file exported from a student information system and, after a run, writes a result file and a package archive. All of this happens in the issuer's browser; no file goes to a server. The flow is described in Bulk import.

FileDirectionFormat
Records fileInCSV, JSON, or a mapped CSV export, read by an adapter
Document folderInA folder of PDF, PNG or JPEG files named by the records file
Result fileOut, and back in to carry on a runimport-result.csv
Package archiveOutcredential-packages.zip

Records file​

Fields​

FieldRequiredLimitMeaning
externalIdyes100 charactersThe row's own key in the source system. Unique in the file.
holderNameyes200The holder's name, kept in the credential package
studentIdyes100The student id. Unique in the file, compared after NFC, trim and upper case.
recordTypeyes200A built-in type or one of the issuer's own types, by name, case-insensitive
programmeno500Display text for the package
awardno500Display text for the package
documentno255A plain file name in the chosen document folder. Empty means a credential without a document.
awardedOnno10Award date, YYYY-MM-DD, kept in the package and not on the chain

Rules that stop a file​

Every row is checked before the wallet is asked for anything, and every problem is listed with its row and field (File for problems with the whole file).

RuleMessage (examples)
Required value missing"The holder name is required"
Text too long"The programme is longer than 500 characters"
Starts with =, +, -, @, a tab or a carriage return"Text may not start with = + - @ or a tab (a spreadsheet would run it as a formula)"
Control or hidden direction characters"Text may not contain control characters or hidden text direction marks"
Unknown record typeThe record type "..." is not a built-in type or one of your types
Name shared by two of the issuer's typesThe record type "..." matches more than one of your record types; rename one of them
Document name is a path or starts with a dot"The document must be a plain file name"
Document not in the folderThe document "..." is not in the chosen folder
Bad date"The award date must be a calendar date written YYYY-MM-DD"
Repeated external idThe external id "..." already appears on row N
Repeated student id"This student id already appears on row N (ids are compared without case or spaces)"
A column or key spelled almost like a field (documents, awarded_on)The column "..." looks like "..." but does not match it; rename it
More than 5,000 rows"The file has more than 5000 records; split it"
More than 10 MB"The file is larger than 10 MB."
No rows"The file has no records"

Warnings do not stop a run: columns or keys that are not used, a document file used by several rows (each of those holders then receives the key to the same file), and document names that were not checked because no folder was given ("The document files were not checked: no folder was given").

Adapters​

An adapter (SisAdapter in src/lib/import/adapter-core.ts) turns text into records. It has an id, a label and parse(text), which returns records and warnings or a list of problems. Adapters read text only, with no network, key or clock, and the list is fixed in ADAPTERS (src/lib/import/adapters.ts).

IdLabel in Export formatReads
csvCSV file with the standard columnsCSV whose header row holds the field names, in any order and letter case
jsonJSON file: an array of records with the standard fieldsA JSON array of objects. externalId and studentId may be whole numbers; other fields must be text. A leading byte order mark is ignored.
templateExample export (Student No, Full Name, Programme, Credential, File, Awarded)A CSV with other header names, mapped by createCsvAdapter. Student No fills both externalId and studentId; the template has no award column.

The CSV reader is the project's own (src/lib/import/csv.ts): cells may be quoted with ", a doubled "" is a literal quote, a quote inside an unquoted cell is an error, and every row must have as many cells as the header. Cells are trimmed. A header that appears twice, or a required column that is missing, stops the file.

To support another system, call createCsvAdapter({ id, label, columns }) with that system's header names, add it to ADAPTERS, and add a test with a sample export.

Example CSV​

Fake data.

externalId,holderName,studentId,recordType,programme,award,document,awardedOn
2026-0001,Asha Rai,S-1001,Degree,BSc Computer Science,First Class,asha-rai.pdf,2026-07-15
2026-0002,Bikash Gurung,S-1002,Transcript,BSc Computer Science,,bikash-gurung.pdf,2026-07-15
2026-0003,Chandra Tamang,S-1003,Certificate,Short Course in Data,,,

Example JSON​

[
{ "externalId": 1, "holderName": "Asha Rai", "studentId": "S-1001", "recordType": "degree", "document": "asha-rai.pdf" },
{ "externalId": 2, "holderName": "Bikash Gurung", "studentId": "S-1002", "recordType": "Transcript" }
]

Checking without a wallet​

npm run import:check -- records.csv
npm run import:check -- export.txt --adapter template --folder ./documents --types "Short Course, Transcript Addendum"

The command prints counts, warnings and every problem with its row and field, never the holders' details. It exits with 0 for a clean file, 1 for a file with problems and 2 for wrong arguments or an unreadable file. Issuer types are passed with --types and numbered from 1000 in the order given; the page reads them from the chain instead.

Row keys​

A row is identified by what it would issue, not by its external id:

rowKey = first 16 bytes (32 hex) of keccak256( JSON.stringify([display, recordTypeId, documentName]) )

display holds the holder name, student id, programme, award and date. A corrected row is therefore a new row. The progress kept in the browser and the result file both use these keys, so neither holds a readable name or id. A separate record, keyed by a hash of the external id, remembers which credential an external id was last issued as; that is how a changed row is recognised.

Result file​

import-result.csv, downloaded with Download result file. One line per row of the records file.

ColumnMeaning
externalIdFrom the records file
credentialIdThe credential id when one exists
stateissued, skipped (already done), recovered (found on chain), failed, unconfirmed or not-run
reasonWhy a row failed or is unconfirmed, or a note (for example a disclosure signature that was not added)
rowKeyThe row key above

Every cell that starts with a formula character is prefixed with an apostrophe, and cells with commas, quotes or line breaks are quoted, so the file is safe to open in a spreadsheet.

Chosen again as Earlier result file, the file carries a run on in another browser. It is read as data, not trusted (readProgress in src/lib/import/progress.ts): rows in state issued, skipped or recovered become "issued" progress, unconfirmed rows become pending attempts, and every credential id is still confirmed on the chain for the issuer before a row is skipped. A file without the four columns, with a malformed row key, or listing a row twice is refused ("This is not a result file written by an earlier import.", "This result file lists the same row twice.").

externalId,credentialId,state,reason,rowKey
2026-0001,0x00000000000000000000000000000000000000000000000000000000000c0301,issued,,0123456789abcdef0123456789abcdef
2026-0002,,failed,The credential could not be issued,fedcba9876543210fedcba9876543210
2026-0003,,not-run,,00112233445566778899aabbccddeeff

Package archive​

credential-packages.zip, downloaded with Download packages (N). It holds one credential package per credential issued in this run, named NNNN-<externalId>.json where NNNN is the row number padded to four digits and the external id is reduced to letters, digits, dot, underscore and hyphen. Packages of skipped or recovered rows are not in it: their salts and keys were never kept and cannot be rebuilt from the chain.

Keep the archive private

Each package holds the key that opens its holder's document and the holder's details. The page says: send each holder only their own package. Finish stays locked until the archive is downloaded, and the browser asks to confirm before leaving the page.

The zip is written by the project's own writer (src/lib/import/zip.ts).

Source files​

  • src/lib/import/adapter-core.ts, adapters.ts, csv-adapter.ts, json-adapter.ts, csv.ts: reading
  • src/lib/import/record.ts, validate.ts, prepare.ts, text.ts: limits and rules
  • src/lib/import/plan.ts, store.ts: row keys and progress record
  • src/lib/import/outputs.ts, progress.ts, zip.ts: result file, archive, carrying on
  • src/lib/import/check-cli.ts, scripts/import-check.ts: npm run import:check
  • docs/import/README.md: the handbook guide

Tests​

  • src/lib/import/adapters.test.ts, csv.test.ts, validate.test.ts, prepare.test.ts, plan.test.ts, outputs.test.ts, progress.test.ts, store.test.ts, check-cli.test.ts, changed.test.ts
  • E2E: e2e/journeys/bulk-import.journey.ts