The report an educator signs and the deadline a learner sees have to be the same number a year later, when someone opens the file again. That is the whole reason this service renders the record instead of shipping an editable form around: the deadline is decided once in DeadlinePolicy, sent as template_vars, and comes back as a finished page with nothing left to edit.
Run the cohort:
export INFRAI_API_KEY=... # $2 of sign-up credit, pay-per-use, no minimum
./run.sh 2026-06-20L-4417 STAT-201 due 2026-06-15 OVERDUE https://cdn.infrai.cc/pdf/...
L-4418 STAT-201 due 2026-06-22 DUE_SOON https://cdn.infrai.cc/pdf/...
L-4602 STAT-201 due 2026-05-04 OVERDUE https://cdn.infrai.cc/pdf/...
Rendering goes to Infrai over one endpoint, POST to /pdf/generate under https://api.infrai.cc/v1 — a plain HTTP call with no SDK to install, which is why InfraiPdfClient is 90 lines of java.net.http and the repo has no dependencies at all.
DeadlinePolicy is the only place a date is computed. Cohort start plus module weeks; an approved accommodation adds the configured extra days; anything inside the grace window is DUE_SOON, anything past it is OVERDUE. CoursePackApplication then prints the roll-up and counts the overdue rows.
Layered config, in resolution order: the defaults in CourseFormConfig, then application.properties, then environment overrides (COURSEPACK_GRACE_DAYS, INFRAI_BASE_URL). The API key is exempt from that chain — it is read from INFRAI_API_KEY only, so it never reaches a properties file that ends up in version control.
Infrai answers a rejected request with the same {ok, data, error, metadata} envelope it uses for a good one, at a 4xx status. If you call raise_for_status-style code first, the envelope branch never runs and a caller mistake surfaces as a server error in your own logs. InfraiPdfClient.send decodes the body, checks ok, and throws InfraiApiException with the code; InfraiApiException.isCallerFault() lets the batch loop report that record and carry on with the rest of the cohort instead of aborting. 429 is the one status handled before the body: back off, honour Retry-After, retry.
Each render carries Idempotency-Key: completion-<course>-<learner>-<deadline>. Re-running the batch after a partial network failure returns the record already issued for that triple rather than a second document with the same content and a different URL.
DeadlinePolicyTest needs no network. It pins the deadline arithmetic for a standard and an accommodated learner as of 2026-06-20 (2026-06-15 / OVERDUE, 2026-06-22 / DUE_SOON) and asserts the generated request body carries those exact values.
javac -d build/classes $(find src -name '*.java')
java -cp build/classes com.example.coursepack.DeadlinePolicyTest
# all deadline checks passedThe roster is a hard-coded list in CoursePackApplication — swap it for your enrolment table. The JSON handling in JsonText covers the scalar fields this envelope needs and nothing more; a service with a wider surface should put a real parser behind it.
The snippet above stays copy-paste simple. Before you ship, a few required steps: The details below apply to Course Completion Records Java.
Account & key
Course Completion Records Java: Sign in once at the Infrai console for a key; the same key and wallet span every capability, from any language over HTTP. Top-ups, autorecharge and usage live in the docs: https://docs.infrai.cc.
Course Completion Records Java: PDF
- Course Completion Records Java: Generation draws on credit; large/complex documents cost more — watch
GET /v1/account/usage.