dataroom.dev

Use case

Building a programmable board portal: recurring distribution, signed packets, engagement audit

Replace the "board pack PDF in an email attachment" pattern with a programmable board portal: scheduled distribution, per-director links with watermarks, audit-ready engagement logs, programmatic revocation when directors roll off the board.

Read full docsBuild a board portal with the API: full reference
May 15, 2026·7 min read·By dataroom.dev

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:

  1. One dataroom per board (the persistent container). Most boards never need more than one dataroom. Meetings are folders within it.
  2. Folders organized by meeting date: typically using ISO-quarter naming like 2026-Q2, 2026-Q3. Some boards use meeting-date naming like 2026-04-22-quarterly for board-of-directors meetings and separate folders for committee meetings (2026-04-15-audit-committee).
  3. Documents versioned per meeting: the board deck, financial pack, committee reports, prior minutes, draft resolutions. A typical board pack runs 50-200 pages.
  4. 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.
  5. View analytics queried before each meeting to confirm pre-read engagement and nudge directors who haven't opened the materials.
  6. 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