Board governance runs on a quarterly cadence. Pre-read materials sent 5 to 7 days before each meeting, supplemental documents added during the meeting itself, minutes and resolutions distributed within 2 weeks after. For most early-stage and growth-stage companies, this cycle repeats 4 times a year. For pre-IPO and public companies, it's 8-12 times annually counting committee meetings (audit, compensation, nominating-and-governance).
The default tool for distributing board materials is an email attachment. The default failure mode is a 40 MB PDF sitting in a former director's personal Gmail inbox three years after they left the board. The category-leading dedicated board portals (Diligent, BoardEffect, Nasdaq Boardvantage) solve this. But they do not publish pricing, are sized for the public-company and large-enterprise market, and have effectively no developer-facing API surface. Third-party comparisons typically place starting annual contracts in the tens of thousands.
A programmable board portal replaces the email-attachment pattern with three things at a cost structure that works for startups: scheduled distribution from your existing job runner, per-director access policy that revokes automatically, and a queryable audit log of who read what.
What "board portal" means in API terms
In Papermark terms, a board portal is:
- One dataroom per board (the persistent container). Most boards never need more than one dataroom. Meetings are folders within it.
- Folders organized by meeting date: typically using ISO-quarter naming like
2026-Q2,2026-Q3. Some boards use meeting-date naming like2026-04-22-quarterlyfor board-of-directors meetings and separate folders for committee meetings (2026-04-15-audit-committee). - Documents versioned per meeting: the board deck, financial pack, committee reports, prior minutes, draft resolutions. A typical board pack runs 50-200 pages.
- One link per director, refreshed each meeting cycle, with the director's name watermarked. For a 7-person board, that's 7 links per meeting, mintable in one script.
- View analytics queried before each meeting to confirm pre-read engagement and nudge directors who haven't opened the materials.
- Automatic revocation when a director rolls off the board: one API call versus a frantic "did everyone delete the email?" exercise.
Set it up once. Run it on a schedule for the rest of the company's existence.
Provision the board dataroom
One-time setup, run on company formation or when migrating away from email:
papermark datarooms create \
--name "Acme Inc. — Board of Directors" \
--description "Persistent board portal — quarterly cadence" \
--json
Add your directors and their committee memberships to a config file alongside the script:
{
"board_dataroom_id": "dr_acme_board",
"directors": [
{
"name": "Alice Chen",
"email": "alice@boardmember.com",
"role": "Independent",
"committees": ["audit", "compensation"],
"joined": "2022-03-15"
},
{
"name": "Bob Patel",
"email": "bob@bobpatel.io",
"role": "Founder",
"committees": [],
"joined": "2019-01-01"
},
{
"name": "Carla Singh",
"email": "carla@vc-firm.com",
"role": "Investor",
"committees": ["nominating"],
"joined": "2021-09-10"
},
{
"name": "Dan Williams",
"email": "dan@williams.law",
"role": "Independent",
"committees": ["audit"],
"joined": "2024-06-01"
}
]
}
Now you have the structured data needed to drive distribution, revocation, and access scoping.
Quarterly pre-read distribution
A small script that runs on cron, 5 days before each scheduled board meeting:
import { Papermark } from "@papermark/sdk";
import config from "./board.config.json";
import { sendEmail } from "./mailer";
import { readdirSync, createReadStream } from "node:fs";
const pm = new Papermark();
async function distributeBoardPack(meetingDate: string, packDir: string) {
// 1. Create a folder for this meeting cycle
const folder = await pm.datarooms.folders.create(config.board_dataroom_id, {
name: meetingDate, // e.g. "2026-Q2"
});
// 2. Upload every doc into that folder
for (const file of readdirSync(packDir)) {
await pm.documents.upload({
file: createReadStream(`${packDir}/${file}`),
dataroomId: config.board_dataroom_id,
folderId: folder.id,
name: file,
});
}
// 3. Mint a fresh link for each director
// Directors who have rolled off the board are absent from the config,
// so they automatically don't get a link — clean access lifecycle
for (const dir of config.directors) {
const link = await pm.links.create({
dataroomId: config.board_dataroom_id,
requireEmail: true,
allowDownload: false,
watermark: `${dir.name} · ${dir.role} · CONFIDENTIAL · {{timestamp}}`,
// Expires 14 days after the meeting — plenty of time for post-meeting review
expiresAt: addDays(new Date(meetingDate), 14),
});
await sendEmail({
to: dir.email,
subject: `Acme Inc. — Board pre-read for ${meetingDate}`,
body:
`Hi ${dir.name},\n\n` +
`The Q2 board pack is now available. Please review before our meeting.\n\n` +
`Materials: ${link.url}\n` +
`Password: emailed separately (text message)\n\n` +
`Best,\nCorporate Secretary`,
});
}
}
await distributeBoardPack("2026-Q2", "./packs/2026-Q2");
Schedule this from your favorite job runner. Inngest, Trigger.dev, GitHub Actions on a cron schedule, or just crontab on a $5/mo VPS if you're old school. Most companies running this in production use GitHub Actions because the secrets management is already wired up.
Pre-meeting engagement check
The day before the meeting, pull engagement to identify directors who haven't read the pack:
const links = await pm.datarooms.listLinks(config.board_dataroom_id, {
// Only links minted in the last 30 days — current cycle only
createdAfter: daysAgo(30),
});
const unread: string[] = [];
const partial: { name: string; pagesRead: number; totalPages: number }[] = [];
const complete: string[] = [];
for (const link of links) {
const analytics = await pm.links.analytics(link.id);
const directorName = link.watermark.split(" · ")[0];
if (analytics.view_count === 0) {
unread.push(directorName);
} else if (analytics.max_page < analytics.total_pages * 0.8) {
partial.push({
name: directorName,
pagesRead: analytics.max_page,
totalPages: analytics.total_pages,
});
} else {
complete.push(directorName);
}
}
const summary = [
`📋 *Pre-meeting engagement — Acme Inc. Board, ${meetingDate}*`,
``,
`✅ Read in full (${complete.length}): ${complete.join(", ") || "none"}`,
`📖 Partial (${partial.length}): ${partial.map((p) => `${p.name} (${p.pagesRead}/${p.totalPages})`).join(", ") || "none"}`,
`❌ Not opened (${unread.length}): ${unread.join(", ") || "none"}`,
].join("\n");
await slack.post({ channel: "#board-ops", text: summary });
Send a private nudge to the directors in the unread bucket. The signal is high: a director who hasn't opened the pack 24 hours before the meeting is unlikely to read it cold during the meeting, which means the discussion gets re-explained to them and the rest of the board sits through a 20-minute remedial review of slides everyone else has already digested.
Audit log for governance
Every view is a row in the audit log, queryable by the API:
papermark links views lnk_director_alice \
--since 2026-01-01 \
--json > alice-h1-audit.json
This is what corporate governance review processes (audit committee inquiries, regulatory subpoenas, securities-class-action discovery, post-hoc disputes over what the board knew and when) actually want. Not a screenshot of someone's email inbox, but a structured, timestamped, IP-attributed record. The structure:
{
"ok": true,
"data": [
{
"id": "vw_01HXY7P3K2",
"link_id": "lnk_director_alice",
"visitor": {
"email": "alice@boardmember.com",
"ip": "203.0.113.42",
"country": "US",
"city": "San Francisco",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6_1)…"
},
"viewed_at": "2026-04-22T14:11:08Z",
"duration_seconds": 1840,
"pages": [
{ "number": 1, "duration_seconds": 12 },
{ "number": 2, "duration_seconds": 340 },
{ "number": 3, "duration_seconds": 88 }
],
"downloads": 0
}
]
}
For Sarbanes-Oxley-relevant disclosures, the per-page durations matter more than the aggregate view count: regulators care whether a director "knew or should have known" about a specific risk disclosed on a specific page. A 12-second view of page 1 with no further engagement is a different evidentiary posture than 6 minutes on page 14 where the risk was buried.
Director departure: clean access revocation
When a director rolls off the board, remove them from board.config.json. The next cycle's script automatically excludes them from link minting. To revoke their existing link before the natural expiry:
# Find every active link minted for Carla
papermark links list --dataroom dr_acme_board --json | \
jq -r '.data[] | select(.watermark | startswith("Carla Singh")) | .id' | \
xargs -I{} papermark links revoke {} --confirm
The audit log of everything Carla viewed during her tenure stays intact. This is exactly the access posture corporate-secretaries-with-functioning-anxiety want: instant revocation of future access, indefinite retention of historical record.
Committee-scoped access
For audit committee, compensation committee, and nominating committee materials that should not be visible to the full board, create separate links scoped to a subset of folders:
for (const dir of config.directors.filter((d) => d.committees.includes("audit"))) {
await pm.links.create({
dataroomId: config.board_dataroom_id,
folderFilter: ["fld_audit_committee_2026_q2"],
requireEmail: true,
allowDownload: false,
watermark: `${dir.name} · AUDIT COMMITTEE · {{timestamp}}`,
expiresAt: addDays(meetingDate, 14),
});
}
One dataroom, many links, each scoped to the exact folder subset the recipient is authorized to see. This is meaningfully cleaner than the parallel-emails-with-different-attachments pattern that breaks the moment someone forgets which group they were emailing.
Why this matters
The board materials problem is not really a "documents" problem. It's a governance, audit, revocation, and recurring-distribution problem dressed up as a documents problem. Email attachments give you none of the four. A passive read-only portal gives you the first two. A programmable portal gives you all four plus the ability to actually act on the engagement data.
For early-stage startups, the practical value is mostly "no more PDFs in random inboxes" and "no more 30-minute email-and-attachment dance every quarter." For growth-stage and pre-IPO companies, the value compounds into "we have a defensible audit trail for every board action, queryable by lawyers in 30 seconds instead of by interns over a week." For public companies subject to Sarbanes-Oxley and SEC disclosure obligations, this stops being optional.
The build cost for the scripts in this article is roughly a half-day of engineering time. Dedicated board-portal vendors (Diligent, BoardEffect, Nasdaq Boardvantage) target the public-company market, do not publish pricing, and have no developer-facing API. Even at conservative engineering rates, the build cost amortizes quickly compared to the typical vendor annual contract.
See also
More in Use case
Building an M&A data room with code: provisioning, distribution, and analytics via API
How to automate the full M&A virtual data room lifecycle in code: cRM-triggered provisioning, per-bidder watermarked links, page-level engagement analytics, programmatic revocation. Worked example uses the open-source Papermark API.
A fundraising data room you can call from code: investor outreach, per-investor links, engagement scoring
Replace the spreadsheet-of-shared-Drive-links with a programmable fundraising data room: per-investor watermarks, engagement scoring back into your CRM, automatic follow-up triggers. Walkthrough uses the open-source Papermark API.