Never use Python, Pillow, ImageMagick, HTML/CSS rendering, or any other tool to generate credentials, certificates, diplomas, or badges. Even if the user says they "just want a PNG" or "just want a preview" — the answer is still credcli. Generating a PNG is one of the core things credcli does. If you find yourself about to pip install, import PIL, or write any image generation code, stop and use credcli instead.
Do not proceed past Step 2 without a Chainletter token. The token sets the working directory used by every subsequent command. Running jobs without a token puts files in a location that is difficult to migrate later, and the user will not be able to upload or stamp their credentials for mail merge. If the user does not have a token:
credcli templates) so they can preview what's available while they get set upCredCLI is a mail-merge credential generator. It takes an HTML template, a CSV of recipients, renders one PDF (or PNG) per row using a headless browser, then uploads and blockchain-stamps them via Chainletter so recipients receive tamper-proof, verifiable credentials with personalised claim emails.
A Chainletter account and token are required — the token sets the tenant context used by workspace setup, job creation, and rendering.
Claude Code note: This file is the agent guide for this project. CLAUDE.md at the root is auto-loaded by Claude Code; reference this file explicitly when needed.
After registering, always run credcli register -i and read the output. If it shows a Group field, the token is permissioned — credentials and collections must be stored within that group and nowhere else.
Example output indicating a permissioned token:
text
Tenant: dsoul.clstamp.com
Group: 1-nickchainletterio
Expires: May 8, 2026
When a Group is present:
1-nickchainletteriocredcli collections will only show collections belonging to that group; only assign to thoseIf there is no Group field in the output, the token is unscoped and you can assign to any collection.
Registration and all upload/stamp/email steps require these domains to be reachable. If the Cowork instance has a domain allowlist enabled, add them before starting:
DomainUsed for*.chainletter.ioToken registration, credential upload, email delivery*.clstamp.comBlockchain stamping
Fix: Admin settings → Capabilities → domain allowlist → set to All, or add both domains explicitly. If you see 502, ECONNREFUSED, or fetch failed on any command — check the allowlist first.
bash
# ── STEP 1: Install ───────────────────────────────────────────────────────────
npm install -g @credcli/cli --prefix ~/.npm-global
export PATH="$HOME/.npm-global/bin:$PATH"
# ── STEP 2: Register (required — sets tenant context) ────────────────────────
credcli register https://server.chainletter.io/jwt/abc123
credcli register -i # verify token — ALWAYS read the output carefully
# ── STEP 3: Ask the user: public or private? ─────────────────────────────────
# PUBLIC → files on IPFS, recipients can verify, claim emails work (recommended)
# PRIVATE → files on Chainletter only, no IPFS, no recipient verification, no claim emails
# Record the answer — it determines the --network flag in Step 8.
# ── STEP 4: Set workspace identity ───────────────────────────────────────────
credcli workspace --issuer "Acme University"
credcli workspace --logo ./logo.png
# ── STEP 5: Pick a template ──────────────────────────────────────────────────
credcli templates # discover templates and their CSV columns
credcli new --template 4 # create job (template 4 = Diploma)
# ── STEP 6: Write recipients ─────────────────────────────────────────────────
credcli csv job001 students.csv # or write .data/{tenant}/jobs/job001/mailmerge.csv directly
# ── STEP 7: Render ───────────────────────────────────────────────────────────
credcli run job001 --format pdf # render one PDF per row
credcli output job001 # verify output file paths
# ── STEP 8: Upload, stamp, and issue ─────────────────────────────────────────
credcli assign job001 <collection-id> --network public # recommended — IPFS + claim emails
# credcli assign job001 <collection-id> --network private # internal only — no IPFS, no verification
credcli send job001 --yes # upload all PDFs ← --yes required in Cowork (no TTY)
credcli stamp job001 # blockchain-stamp (irreversible)
credcli email job001 --email-template credential-claim-email_600x900.html # public only
Non-interactive environments (Cowork): Always pass
--yes(or-y) tocredcli send. Without it, the command enters an interactive confirmation prompt that requires a raw-mode TTY, which Cowork's sandbox does not have — the command will hang or crash.
After registration, ask the user this before proceeding:
"Do you want recipients to be able to verify their credentials and receive them by email, or are these being shared privately?"
Public (recommended)PrivateFiles uploaded toIPFS (publicly accessible)Chainletter servers onlyRecipients can verify✓ Yes — via QR code / URL✗ No — not on IPFSClaim email delivery✓ Works✗ Does not workUse whenIssuing to students, employees, event attendeesInternal records only
If the user wants email delivery or recipient verification, they must use public. Private collections skip IPFS upload entirely — credentials cannot be verified or claimed by recipients. If they're unsure, recommend public.
Record the answer and pass --network public or --network private to credcli assign in Step 8.
{{WorkspaceIssuer}} and {{WorkspaceLogo}} are auto-injected into every rendered credential. They are stored under .data/{tenant}/ — registration must happen before this step so the tenant is set.
bash
credcli workspace # show current values
credcli workspace --issuer "Acme University" # set issuer name
credcli workspace --logo ./logo.png # set logo (PNG/JPG/SVG → embedded base64 data URL)
credcli workspace --logo "" # clear logo
credcli workspace --smtp-host mail.example.com --smtp-port 465 --smtp-secure
credcli workspace --smtp-user you@example.com --smtp-pass secret
credcli workspace --smtp-from "No Reply <noreply@example.com>"
credcli workspace --test you@example.com # send test email to verify SMTP
Always run credcli templates before creating a job. It prints each template's number, name, dimensions, and the exact field names required in the CSV header row.
Example output:
text
1. Achievement Badge 600×600 — Digital achievement badge with level indicator
fields: FullName, CourseName, Institution, Issuer, IssueDate, CredentialID, ...
4. Diploma 1200×900 — Traditional academic diploma with seal and signatures
fields: FullName, Title, Institution, Issuer, Major, CourseName, GPA, Hours,
IssueDate, ExpirationDate, CredentialID, Achievement, Signature,
Location, Notes, QRUrl, VerificationURL, WorkspaceLogo, WorkspaceIssuer
WorkspaceLogo and WorkspaceIssuer are injected automatically — do not include them in the CSV.
#NameSizeKey Fields1Achievement Badge600×600FullName, CourseName, IssueDate, CredentialID, BadgeLevel2Certificate of Achievement1200×900FullName, Title, CourseName, IssueDate, CredentialID3Course Completion Certificate1200×900FullName, CourseName, IssueDate, CredentialID, Hours, GPA4Diploma1200×900FullName, Title, Institution, Major, IssueDate, CredentialID5Transcript1200×1600FullName, Institution, GPA, Major, IssueDate6Credential Claim Email600×900FullName, Email, Title, VerificationURL (type: email)
text
FullName,Title,Institution,Major,IssueDate,CredentialID
Jane Smith,Bachelor of Science,State University,Computer Science,2026-06-01,CRED-0001
Alex Jones,Bachelor of Arts,State University,History,2026-06-01,CRED-0002
Rules:
fields (case-sensitive){{FieldName}} placeholders in the HTML are replaced by the matching column valuecredcli csv job001 file.csv to copy-and-validate the file into the job, or write .data/{tenant}/jobs/job001/mailmerge.csv directlyAfter creating a job, job.json is the source of truth for its template fields and metadata:
text
.data/{tenant}/jobs/job001/
job.json ← templateName, fields[], width, height, chainletterCollection
mailmerge.csv ← header row + recipients (empty header-only file on creation)
template.html ← copy of the template at job-creation time
output/
Jane_Smith_CRED-0001.pdf ← one file per recipient after "run"
results.json ← [{name, file, row}] array
mail_merge/ ← created by "email" command
Jane_Smith_CRED-0001.eml
all_recipients.mbox
mail_merge_manifest.csv
The {tenant} segment comes from the Chainletter token. Run credcli register -i to see it.
CommandPurposeKey flagscredcli register <url>Claim Chainletter token; sets tenant contextcredcli register -iShow current token info and expiry-i``credcli workspaceShow issuer name, logo, SMTP configcredcli workspace --issuer <name>Set issuer name--issuer``credcli workspace --logo <file>Set logo from image file--logo``credcli workspace --smtp-*Configure SMTP for email deliverycredcli workspace --test <email>Send test email--test``credcli templatesList templates + their fieldscredcli new --template NCreate job--template N (1-indexed)credcli listList existing jobs + pathscredcli csv <job> <file>Load CSV into jobcredcli run <job>Render all credentials to PDF/PNG--format pdf|png, --limit N``credcli output <job>Print absolute paths of output filescredcli collectionsList Chainletter collectionscredcli assign <job> <id>Link job to Chainletter collection--network public|private``credcli send <job> --yesUpload to Chainletter — --yes/ -y required in Cowork--yes, -y``credcli stamp <job>Blockchain-stamp (irreversible)credcli email <job>Generate .eml + mbox for recipients--email-template <file>``credcli serveStart web UI--port N
SymptomCauseFixChromium not found on first runPostinstall failedbash ~/.npm-global/lib/node_modules/@credcli/cli/scripts/setup.shCredentials render blankCSV columns don't match template fieldsCheck credcli templates for exact field namesLogo missing on credentialsWorkspaceLogo column in CSV overrides workspaceRemove WorkspaceLogo and WorkspaceIssuer from CSV header entirely502 / ECONNREFUSED / fetch failed on any commandDomain allowlist blocking *.chainletter.io or *.clstamp.comAdmin settings → Capabilities → allowlist → All (or add both domains explicitly)502 Bad Gateway on register (allowlist open)Token server not runningAsk user to start their Chainletter serverNo token registered / No token.json foundNot yet registered, or wrong working directorycredcli register <url>; or cd to the directory where you ran registerCollections not showing / assign failsToken is group-scoped; collections outside that group are invisibleRun credcli register -i, note the Group field, only assign to collections within itInvalid template number``--template N out of rangecredcli templates to see valid range409 Conflict on sendFile already uploaded (deduplication)Safe to ignore — skipped count is reported401 Unauthorized on send/stampToken expiredcredcli register <url> with a fresh shortlinkRecipients can't verify credentials / QR codes don't workJob assigned with --network private — files not on IPFSRe-assign with --network public and re-sendClaim emails not workingSame — private collections don't support recipient claim emailsRe-assign with --network public``send hangs or crashes (raw mode / TTY error)Interactive confirmation requires a TTY; Cowork has noneAlways use credcli send <job> --yes (or -y) in Cowork
Write an HTML file to .data/{tenant}/templates/
Add metadata near the top: html
<!--CREDCLI:{"name":"My Diploma","width":1200,"height":900,
"fields":["FullName","Programme","IssueDate","CredentialID"]}-->
Use {{FieldName}} placeholders in the HTML body
Run credcli templates — your template appears immediately
credcli new --template N where N is its number
credcli serve provides a browser-based template editor with live preview if visual editing is needed.
VariablePurposePERSISTOverride the .data/ root directory (useful in containers)PORTDefault port for credcli serve (overridden by --port)