How to Batch-Fill USCIS Immigration Packets (G-28, I-129, I-140, I-485, I-765) from CSV/JSON Without Adobe Acrobat or XFA Desync
If you work in immigration legal ops, corporate mobility HR, or build document automation for law firms, you already know the arithmetic of an employment-based or family-based USCIS filing:
- A single H-1B / L-1 / O-1 nonimmigrant packet (
Form G-28+Form I-129) spans 42 pages and 1,081 interactive PDF fields (101fields inG-28+980fields inI-129). - A single concurrent EB-2 / EB-3 green card filing (
Form G-28+Form I-140+Form I-485+Form I-765) spans 43 pages and 1,283 fields (101 + 261 + 760 + 161). - Across a 40-employee H-1B cap season batch or a 25-family adjustment cohort, paralegals end up re-typing the same petitioner FEIN, attorney bar number, beneficiary passport number,
I-94record, and physical address tens of thousands of times.
Yet when engineering teams try to automate G-28, I-129, I-140, I-485, I-765, or N-400 from a clean CSV, Excel (.xlsx), or JSON payload using standard Python or Node.js PDF libraries, they immediately hit three structural walls baked into official uscis.gov PDFs:
- Owner Encryption +
/PermsUsage-Rights Locks: 24 of 26 USCIS PDFs carry empty-password owner encryption (modifyDenied: true) plus an Adobe/UR3digital signature inside/Root/Perms. - Cryptic LiveCycle Subform Paths: Instead of semantic names like
beneficiary_last_name, fields are nested inside XFA-mirrored trees likeform1[0].#subform[0].Pt1Line1a_FamilyName[0]—and inconsistent across forms (Pt1Line1a_FamilyName[0]on one form vs.Part2_Line1_FamilyName[0]on the next). - Hybrid
/XFAvs./AcroFormDesynchronization: Updating/AcroForm /Fields /Vwithout stripping/AcroForm/XFAcauses Windows Adobe Acrobat to prioritize the empty<xfa:datasets>XML stream and display every populated field 100% blank.
Below is a complete engineering breakdown of how official USCIS packet PDFs are structured, how to extract a unified cross-form schema using /TU (tooltip) metadata, and how to batch-fill multi-form USCIS packets from CSV/JSON in Python or the browser.
1. Structural Anatomy of a 6-Form USCIS Filing Packet
When we audited all 26 USCIS forms in our structural study using pypdf 6.6 and pikepdf, every core petition and application shared the same Adobe LiveCycle / AEM Forms hybrid architecture:
| Form Code | Official Purpose | Pages | Size | Total Fields | Field Breakdown (Tx / Btn / Ch) | Hybrid /XFA | Owner /Encrypt + modifyDenied |
|---|---|---|---|---|---|---|---|
| Form G-28 | Notice of Entry of Appearance as Attorney | 4 | 387 KB | 101 | 76 Tx, 23 Btn, 2 Ch | Yes | Yes |
| Form I-129 | Petition for a Nonimmigrant Worker (H-1B, L-1, O-1) | 38 | 2,242 KB | 980 | 623 Tx, 333 Btn, 24 Ch | Yes | Yes |
| Form I-140 | Immigrant Petition for Alien Workers (EB-1/2/3) | 8 | 513 KB | 261 | 179 Tx, 79 Btn, 3 Ch | Yes | Yes |
| Form I-485 | Application to Register Permanent Residence | 24 | 1,175 KB | 760 | 334 Tx, 416 Btn, 10 Ch | Yes | Yes |
| Form I-765 | Application for Employment Authorization (EAD) | 7 | 456 KB | 161 | 119 Tx, 38 Btn, 4 Ch | Yes | Yes |
| Form N-400 | Application for Naturalization | 14 | 758 KB | 440 | 237 Tx, 198 Btn, 5 Ch | Yes | Yes |
Notice two things right away:
- Zero native
/Btnradio groups (radios: 0): Even mutually exclusive "Yes / No" questions onI-485(416 checkboxes) andI-129(333 checkboxes) are implemented as individual checkbox widgets where each/AP /Ndictionary defines a specific export key (typically/Y,/N,/1, or/Yes, paired with/Off). - Every single form is both
xfa: trueandencrypted: true: Before your script can modify widgets or merge pages into a single filing packet, you must decrypt the empty-password owner lock and remove both/AcroForm/XFAand/Root/Perms.
2. Why One Canonical JSON Record Can't Naive-Match Across USCIS Forms
Suppose you have a canonical JSON or CSV row representing one beneficiary and one sponsoring employer/attorney:
{ "case_id": "2026-H1B-042", "attorney.family_name": "Vance", "attorney.given_name": "Elena", "attorney.bar_number": "CA-294810", "attorney.firm_name": "Vance Global Immigration LLP", "employer.legal_name": "Acme Robotics Inc.", "employer.fein": "94-1234567", "beneficiary.family_name": "Kowalski", "beneficiary.given_name": "Jan", "beneficiary.alien_number": "A-219847561", "beneficiary.dob": "04/19/1991", "beneficiary.i94_number": "84920194821" } If you inspect the /AcroForm /Fields trees across g-28.pdf, i-129.pdf, i-140.pdf, i-485.pdf, and i-765.pdf, the beneficiary's last name lives at a different hierarchical path on every form:
Form G-28 -> form1[0].#subform[1].Pt3Line5a_FamilyName[0] Form I-129 -> form1[0].#subform[2].Part3_Line1a_FamilyName[0] Form I-140 -> form1[0].#subform[1].Pt3Line1a_FamilyName[0] Form I-485 -> form1[0].#subform[0].Pt1Line1a_FamilyName[0] Form I-765 -> form1[0].#subform[0].Pt1Line1a_FamilyName[0] Form N-400 -> form1[0].#subform[0].Pt2Line1_FamilyName[0] Fortunately, USCIS embeds detailed human-readable descriptions inside every widget's /TU (Alternate Field Name / Tooltip) entry for Section 508 accessibility—for example:
312 0 obj << /Type /Annot /Subtype /Widget /FT /Tx /T (Pt1Line1a_FamilyName[0]) /TU (Part 1. Information About You. Line 1a. Family Name (Last Name)) /MaxLen 35 /DA (/CourierStd 10 Tf 0 g) /Rect [ 54.00 582.10 294.00 596.50 ] >> endobj By extracting (full_field_path, /FT, /TU, /MaxLen, /AP/N states) once per form edition, you can build a deterministic Canonical Packet Mapping (packet_map.json) that binds one canonical key (beneficiary.family_name, attorney.bar_number, employer.fein) to its exact target widget across G-28, I-129, I-140, I-485, I-765, and N-400.
3. Python Pipeline: Inspecting & Batch-Filling a Multi-Form USCIS Packet
Below is a complete, self-contained Python pipeline (pikepdf + pypdf) that:
- Unlocks official USCIS PDFs (
G-28,I-140,I-485,I-765,I-129,N-400) by stripping owner encryption,/Root/Perms, and/AcroForm/XFA. - Maps a single canonical CSV/JSON record across every form in the packet.
- Correctly sets both
/Vand/ASon USCIS checkboxes (NameObject("/Y"),NameObject("/1"), etc.) while clearing cached/APappearance streams on modified text fields so every viewer renders the text cleanly.
#!/usr/bin/env python3 """ uscis_packet_batch_filler.py Batch-generate multi-form USCIS immigration packets (G-28, I-129, I-140, I-485, I-765, N-400) from CSV or JSON while stripping hybrid /XFA streams and /Perms UR3 locks. """ import csv import json import sys from pathlib import Path import pikepdf from pypdf import PdfReader, PdfWriter from pypdf.generic import BooleanObject, NameObject, TextStringObject def index_terminal_widgets(node_ref, prefix="", out=None): if out is None: out = {} node = node_ref.get_object() name = str(node.get("/T", "")) full_name = f"{prefix}.{name}" if prefix and name else (name or prefix) if "/Kids" in node: for kid_ref in node["/Kids"]: index_terminal_widgets(kid_ref, full_name, out) else: out[full_name] = node if name: # Also index by terminal key for convenience when unique out.setdefault(name, node) return out def set_uscis_widget_value(widget, raw_value: str) -> None: ft = widget.get("/FT") if ft == "/Btn": ap_n = widget.get("/AP", {}).get("/N", {}) on_states = [k for k in ap_n.keys() if k != "/Off"] truthy = str(raw_value).strip().lower() in ("true", "yes", "y", "1", "x", "checked") target_state = NameObject("/Off") if truthy and on_states: # If caller passed an explicit state like "/Y" or "/1", honor it; else pick first on-state explicit = f"/{str(raw_value).strip().lstrip('/')}" target_state = NameObject(explicit if explicit in on_states else on_states[0]) widget[NameObject("/V")] = target_state widget[NameObject("/AS")] = target_state else: text = "" if raw_value is None else str(raw_value).strip() max_len = widget.get("/MaxLen") if isinstance(max_len, int) and max_len > 0: text = text[:max_len] widget[NameObject("/V")] = TextStringObject(text) # Remove cached blank appearance stream so viewer synthesizes glyphs from /V + /DA if NameObject("/AP") in widget: del widget[NameObject("/AP")] def populate_uscis_form(template_pdf: Path, field_map: dict, record: dict, out_pdf: Path) -> int: # Step 1: Remove owner encryption, /Perms UR3 signature lock, and hybrid /XFA array unlocked_tmp = out_pdf.with_suffix(".unlocked.tmp.pdf") with pikepdf.open(template_pdf, password="") as pdf: if "/Perms" in pdf.Root: del pdf.Root["/Perms"] if "/AcroForm" in pdf.Root and "/XFA" in pdf.Root.AcroForm: del pdf.Root.AcroForm["/XFA"] pdf.save(unlocked_tmp) # Step 2: Populate AcroForm widgets with pypdf reader = PdfReader(str(unlocked_tmp)) writer = PdfWriter() writer.append(reader) root = writer._root_object acroform = root[NameObject("/AcroForm")].get_object() acroform[NameObject("/NeedAppearances")] = BooleanObject(True) widgets = {} for ref in acroform.get("/Fields", []): index_terminal_widgets(ref, "", widgets) filled_count = 0 for canonical_key, pdf_field_path in field_map.items(): val = record.get(canonical_key) if val in (None, ""): continue if pdf_field_path not in widgets: raise KeyError(f"Field '{pdf_field_path}' not found in {template_pdf.name}") set_uscis_widget_value(widgets[pdf_field_path], val) filled_count += 1 out_pdf.parent.mkdir(parents=True, exist_ok=True) with open(out_pdf, "wb") as f: writer.write(f) unlocked_tmp.unlink(missing_ok=True) return filled_count def run_batch(packet_config_path: str, csv_path: str, out_root: str) -> None: """ packet_config.json structure: { "forms": { "G-28": {"template": "./templates/g-28.pdf", "map": {"attorney.family_name": "Pt1Line2a_FamilyName[0]", ...}}, "I-140": {"template": "./templates/i-140.pdf", "map": {...}}, "I-485": {"template": "./templates/i-485.pdf", "map": {...}}, "I-765": {"template": "./templates/i-765.pdf", "map": {...}} } } """ config = json.loads(Path(packet_config_path).read_text(encoding="utf-8")) out_dir = Path(out_root) with open(csv_path, newline="", encoding="utf-8-sig") as f: for row_idx, record in enumerate(csv.DictReader(f), start=1): case_id = record.get("case_id", f"case_{row_idx:03d}") for form_code, form_spec in config["forms"].items(): out_file = out_dir / case_id / f"{case_id}_{form_code}.pdf" n = populate_uscis_form(Path(form_spec["template"]), form_spec["map"], record, out_file) print(f"[{case_id}] {form_code}: populated {n} fields -> {out_file}") if __name__ == "__main__": if len(sys.argv) != 4: print("Usage: python uscis_packet_batch_filler.py packet_config.json cases.csv ./output_packets") sys.exit(1) run_batch(sys.argv[1], sys.argv[2], sys.argv[3]) 4. Handling USCIS LiveCycle Input Masks & Comb Fields (SSN, A-Number, I-94)
When batch-filling I-129, I-140, I-485, I-765, and N-400, watch out for three field-level edge cases inside USCIS PDFs:
- Comb Fields (
/Ffbit 25 =16777216+/MaxLen): Alien Registration Numbers (A-Number, 9 digits), Social Security Numbers (9 digits), USCIS Online Account Numbers (12 digits), and ZIP codes use fixed-cell comb boxes. Strip hyphens and spaces ("A-219-847-561"->"219847561") before writing to/V, or the characters will shift one cell to the right and clip the final digit. - LiveCycle JavaScript Validation Traps on Foreign Addresses &
"Present"Dates: When opened inside Adobe Acrobat with XFA active, LiveCycle scripts validate US state codes andMM/DD/YYYYdate masks—blocking common immigration entries like"PRESENT"in employment history or foreign postal codes. Stripping/AcroForm/XFAdisables those rigid XFA script traps while preserving exact courier comb spacing in/AcroForm. - macOS Preview Desynchronization: If a beneficiary edits an untouched USCIS PDF in macOS Preview, Preview writes to
/AcroForm /Fields /Vwhile leaving<xfa:datasets>empty. Running thepikepdf/XFA+/Permsstrip step above (or using our XFA to Fillable PDF Unlocker) immediately restores visibility in Windows Adobe Acrobat.
5. Browser & Spreadsheet Workflows for Legal Teams and HR Ops
If your immigration practice, in-house HR mobility team, or paralegal staff wants to fill single petitions or batch-generate USCIS packets from .xlsx / .csv spreadsheets without maintaining Python scripts or paying for per-seat desktop PDF licenses:
- Fill PDF from Excel / CSV Batch Mail Merge: Upload any USCIS or employment form alongside an Excel or CSV roster, preview 5 populated rows for free to verify comb alignment and checkbox states, and download a ZIP archive of clean, unencrypted PDFs.
- XFA to Fillable PDF Unlocker: Convert Hybrid XFA and owner-locked government PDFs into standard ISO 32000-1 AcroForm or flattened PDFs that work identically across Chrome, Safari, macOS Preview, and Windows Adobe Acrobat.
- Direct Online Fillers & Field Guides for Core USCIS Packet Forms:
- Form G-28 Online Filler — Notice of Entry of Appearance as Attorney or Accredited Representative (4 pages, 101 fields)
- Form I-129 Online Filler — Petition for a Nonimmigrant Worker: H-1B, L-1, O-1, TN (38 pages, 980 fields)
- Form I-140 Online Filler — Immigrant Petition for Alien Workers: EB-1, EB-2, EB-3 (8 pages, 261 fields)
- Form I-485 Online Filler — Application to Register Permanent Residence or Adjust Status (24 pages, 760 fields)
- Form I-765 Online Filler — Application for Employment Authorization / EAD (7 pages, 161 fields)
- Form N-400 Online Filler — Application for Naturalization (14 pages, 440 fields)
Disclosure: I am the solo developer of JustFill.
AI disclosure: AI coding agents drafted and revised this article and verified the code examples above.