This was created by Taylor Davidson of Hemrock. Distributing K1s to investors can be a pain, so let's make it a bit easier. This script can be run through command line or a web interface running locally on your computer - no data is shared outside of your computer - and it can take a folder of PDF K1s, redact EIN/TIN/SSNs if needed, encrypt and password-protect if needed (creating passwords from the last 4 digits of the K1 recipient's SSN/TIN/EIN and their zip code), and use a template email and a CSV of recipient names and email addresses (including support for multiple contacts per LP), send them from your own Gmail (or SendGrid, if desired).
Questions, ask anytime.
npm installRequires Node.js. Install PDFtk for encryption (or qpdf if you use redaction first):
brew install pdftk-javaCopy the example env file and edit with your values:
cp .env.example .envEdit .env and set at minimum:
| Variable | Required for | Description |
|---|---|---|
FROM_EMAIL |
Gmail, SendGrid | Sender email address |
FROM_NAME |
Gmail, SendGrid | Sender display name |
TEST_SEND_EMAIL |
Test send | Address to receive test emails |
SENDGRID_API_KEY |
SendGrid | SendGrid API key (if using SendGrid) |
Optional:
| Variable | Description |
|---|---|
USE_QPDF |
Set to 1 to use qpdf instead of PDFtk (preserves fonts after redaction) |
CREDENTIALS_PATH |
Path to Gmail OAuth credentials (default: project root) |
TOKEN_PATH |
Path to Gmail OAuth token (default: project root) |
IGNORE_FOLDER |
Base folder for sensitive output (default: ignore) |
FONT_PATH |
Path to redaction overlay font (default: fonts/CourierPrime-Regular.ttf) |
Keep confidential documents in the ignore/ folder (it is gitignored):
- Put K-1 PDFs in a subfolder (e.g.
ignore/2025_fund_name/original/) - Place
credentials.jsonandtoken.jsonin the project root for Gmail (they are gitignored) - Password CSVs are written next to the fund folder (e.g.
ignore/2025_fund/k1_passwords_original.csv)
A minimal web interface lets you run prepare, test-match, and send operations from the browser.
Start the UI:
npm run uiOpen http://localhost:3000 (or the next available port if 3000 is in use).
How it works:
- Prepare — Choose redact only, encrypt only, or both. Select the folder containing your original PDFs (e.g.
ignore/2025_fund/original). The UI runs the same scripts as the CLI. - Gmail authorization — If you have
credentials.jsonbut notoken.json, the UI shows an authorization section. Click "Authorize Gmail", sign in with Google — you're redirected back automatically. Add the shown redirect URI to your Google Cloud Console OAuth client if needed. - Test matching — Select the
_protectedor_redacted_protectedPDF folder and LP CSV to verify each LP has exactly one matching PDF. - Test send — Send one LP's K-1 to a test address. Set the test email in the UI (or it uses
TEST_SEND_EMAILfrom.env). Pick which LP by row number. - Full send — Send all K-1s via Gmail. Requires confirmation before sending.
Dropdowns list folders and files from ignore/ and example/. The UI calls the underlying Node scripts; no PDFs or passwords are sent to the browser.
Security:
- The UI runs on localhost only — it does not listen on external interfaces.
- All processing happens on your machine. The browser only sends form data (paths, options) to the local server.
- PDFs, passwords, and credentials stay on disk. The server spawns the same CLI scripts you would run manually.
- Use the UI only on a trusted machine. Do not expose the server to a network.
- Prepare K-1 PDFs: redact (optional) and encrypt
- Test matching to verify PDF filenames match your LP CSV before sending
- Send K-1s via Gmail or SendGrid
One-step or step-by-step preparation of K-1 PDFs.
Usage: npm run prepare-k1s -- <input_path> (input path is required)
| Command | Description |
|---|---|
npm run prepare-k1s -- ignore/2025_fund |
Redact, then encrypt (both) |
npm run prepare-k1s-redact -- ignore/2025_fund |
Redact only |
npm run prepare-k1s-encrypt -- ignore/2025_fund_redacted |
Encrypt only |
For --both (default): input is the original PDF folder; output is ..._redacted then ..._redacted_protected.
Redacts the receiving party's SSN/TIN (the second identifier on the page) when it is currently unredacted. Covers the number with a white rectangle and prints the redacted form on top (e.g. **-***0337 for TIN, ***-**-7876 for SSN). PDFs where the second identifier is already redacted are left unchanged.
Usage: node redact_k1.js <input_path>
- input_path: Single PDF or folder of PDFs
- Output:
<filename>_redacted.pdfor<folder>_redacted/
Run redaction before encryption if you use it.
Encrypts K-1 PDFs with a password derived from SSN/TIN last 4 digits and ZIP code extracted from each PDF.
Usage: node k1script.js [input_folder]
- input_folder: Defaults to
ignore/originalif omitted - Output:
<input_folder>_protected/andk1_passwords_<folder>.csv(saved in the parent of the input folder)
Encryption tool: PDFtk (default) or qpdf. Set USE_QPDF=1 in .env to use qpdf (preserves fonts after redaction). Install with brew install qpdf.
Tests that each LP in your CSV has exactly one matching PDF (by filename). Run before sending.
Usage: node test_k1s.js <pdf_folder> [lp_csv]
- pdf_folder: Folder containing K-1 PDFs (e.g. the
_protectedfolder) - lp_csv: Defaults to
lp_list.csv
Output: matching_results.csv next to the LP CSV.
You need: email template, K-1 PDFs, and LP CSV.
identifier,email
LP001,john.doe@example.com
ACME_LLC,contact@acme.com
"ACME LLC",contact@acme.com;finance@acme.com- identifier must match part of the K-1 PDF filename
- Use semicolons (
;) for multiple emails per LP - Wrap values with commas in double quotes
See example_list.csv for a sample.
First line: SUBJECT: Your subject. Remaining lines: body. See email_template.txt for a template.
node send_k1s_gmail.js <pdf_folder> [lp_csv] [email_template]Setup:
- Google Cloud Console: create OAuth credentials (type "Web application"), download as
credentials.json - Add the redirect URI to your OAuth client:
http://localhost:3000/auth/gmail/callback(and:3001,:3002if the UI uses those ports — the UI shows the exact URI to add) - Place
credentials.jsonin the project root (gitignored) - Complete authorization (see flow below)
Both the terminal and web UI create the same token.json file. Once authorized, either interface can send emails.
Terminal flow (for send_k1s_gmail.js or send_k1s_gmail_test.js):
- Run the script. If
token.jsondoesn't exist, it prints a URL. - Open the URL in your browser and sign in with Google.
- Google may show "This site can't be reached" or a blank page at localhost — that's expected. Copy the entire code from the address bar (the part after
code=and before&scope). - Paste the code into the terminal when prompted and press Enter.
- The script saves
token.jsonand proceeds. Future runs use the stored token; no re-authorization unless you delete it or it expires.
Web UI flow (recommended):
- Start the UI (
npm run ui) and open http://localhost:3000. - If
token.jsondoesn't exist, an "Authorize Gmail" section appears. - Click "Authorize Gmail". You're sent to Google to sign in.
- After approving, Google redirects you back to the app. The server captures the code from the URL, exchanges it for tokens, saves
token.json, and shows a success message. - No copy-paste. If the redirect fails (e.g. redirect URI not configured), use "Redirect not working? Use paste flow" to paste the code manually.
Credentials type: The redirect flow requires "Web application" credentials with the callback URI added in Google Cloud Console. If you have "Desktop app" credentials instead, use the paste flow in the web UI or authorize via the terminal.
Send one LP's K-1 to TEST_SEND_EMAIL to review before full send:
node send_k1s_gmail_test.js <pdf_folder> [lp_csv] [email_template] [lp_pick]lp_pick: number (1-based) or part of identifier.
node send_k1s_sendgrid.js <pdf_folder> [lp_csv] [email_template]Requires SENDGRID_API_KEY in .env. Authenticate your domain in SendGrid to avoid spoof warnings.
This can be edited to use Resend or other email providers.
| Script | Description |
|---|---|
npm run prepare-k1s -- <path> |
Redact + encrypt |
npm run prepare-k1s-redact -- <path> |
Redact only |
npm run prepare-k1s-encrypt -- <path> |
Encrypt only |
npm run redact -- <path> |
Redact (direct) |
npm run encrypt -- [path] |
Encrypt (direct) |
npm run test-match -- <pdf_folder> [lp_csv] |
Test PDF/LP matching |
npm run send-gmail -- ... |
Send via Gmail |
npm run send-gmail-test -- ... |
Test send via Gmail |
npm run send-sendgrid -- ... |
Send via SendGrid |
npm run ui |
Start web interface (localhost:3000) |
Pass arguments after --.
The repo's .gitignore includes:
ignore/– K-1 PDFs, password CSVscredentials.json,token.json– Gmail OAuth (project root).env– environment variables
Never commit these files.
