dataroom.dev

Cookbook

One-script provisioning: create, populate, and share a virtual data room in 60 seconds

A copy-pasteable end-to-end script that creates a data room, uploads a folder of files, mints a tracked link, and prints the URL: bash, Node, and Python variants. Implementation against the Papermark API.

Read full docsRead the full CLI and API quickstart
April 22, 2026·7 min read·By dataroom.dev

This is the shortest path from "I have a folder of PDFs and an email address" to "the recipient has a tracked, watermarked, gated link." Three implementations, in increasing order of language richness: Bash + curl + jq, the Papermark CLI, TypeScript with the SDK, and Python with the SDK. Pick the one that matches your environment. Copy-paste-modify.

The use cases that come up most often for this script:

  1. One-off investor share: a founder sending the deck to a specific VC mid-pitch.
  2. Customer-specific deal room: a sales team sending a tailored kit to a prospect.
  3. Vendor due-diligence packet: compliance/security teams responding to a SOC 2 questionnaire.
  4. Board pre-read distribution: corporate secretary running the quarterly cycle.
  5. Legal document delivery: outside counsel sending privileged materials to a client.
  6. CI artifact distribution: engineering teams sharing build artifacts or compliance reports with downstream consumers.
  7. Bulk-mode in a loop: wrap any of the above in a for loop over a recipients CSV.

All seven boil down to the same three API calls: create dataroom, upload documents, mint link.

Bash + curl + jq

The lowest-dependency variant. Requires only curl (preinstalled on every modern OS) and jq (one brew install jq or apt install jq away).

#!/usr/bin/env bash
set -euo pipefail

# ─── Inputs ─────────────────────────────────────────────────────────
NAME="${1:?usage: $0 'dataroom name' ./folder recipient@example.com}"
DIR="${2:?usage: $0 'dataroom name' ./folder recipient@example.com}"
RECIPIENT="${3:?usage: $0 'dataroom name' ./folder recipient@example.com}"
API="https://api.papermark.com/v1"

: "${PAPERMARK_TOKEN:?
  set PAPERMARK_TOKEN — get one at https://app.papermark.com/settings/tokens
}"

# ─── 1: create the dataroom ─────────────────────────────────────────
echo "→ creating dataroom \"$NAME\""
DR_ID=$(curl -sS -X POST "$API/datarooms" \
  -H "Authorization: Bearer $PAPERMARK_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"$NAME\"}" | jq -r '.data.id')
echo "  dataroom $DR_ID"

# ─── 2: upload every supported file in the folder ───────────────────
SUPPORTED=("pdf" "docx" "pptx" "xlsx" "csv" "txt" "md")
UPLOADED=0
for f in "$DIR"/*; do
  [ -f "$f" ] || continue
  ext="${f##*.}"
  ext_lower=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
  if [[ ! " ${SUPPORTED[*]} " =~ " $ext_lower " ]]; then
    echo "  skipping unsupported: $f"
    continue
  fi
  echo "→ uploading $(basename "$f")"
  curl -sS -X POST "$API/documents" \
    -H "Authorization: Bearer $PAPERMARK_TOKEN" \
    -F "file=@$f" \
    -F "dataroom_id=$DR_ID" > /dev/null
  UPLOADED=$((UPLOADED + 1))
done
echo "  uploaded $UPLOADED documents"

# ─── 3: mint a tracked link ─────────────────────────────────────────
echo "→ minting link for $RECIPIENT"
LINK_URL=$(curl -sS -X POST "$API/links" \
  -H "Authorization: Bearer $PAPERMARK_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"dataroom_id\": \"$DR_ID\",
    \"require_email\": true,
    \"allow_download\": false,
    \"watermark\": \"$RECIPIENT · {{timestamp}}\",
    \"notes\": \"Generated for $RECIPIENT on $(date -u +%Y-%m-%dT%H:%M:%SZ)\"
  }" | jq -r '.data.url')

echo
echo "✓ done in $SECONDS seconds"
echo "  dataroom: $DR_ID"
echo "  documents: $UPLOADED"
echo "  link:     $LINK_URL"

Run it:

chmod +x share.sh
export PAPERMARK_TOKEN=pm_live_…
./share.sh "Acme — Series A" ./acme-pack alice@vc.com

For a typical 8-document dataroom on a broadband connection, this completes in 25-60 seconds. The bottleneck is upload time, not API latency.

Same thing with the CLI

If you have papermark installed (npm install -g papermark), the script collapses to about 12 lines:

#!/usr/bin/env bash
set -euo pipefail

NAME="$1"; DIR="$2"; RECIPIENT="$3"

DR=$(papermark datarooms create --name "$NAME" --json | jq -r '.data.id')

for f in "$DIR"/*; do
  [ -f "$f" ] && papermark documents upload "$f" --dataroom "$DR" > /dev/null
done

papermark links create \
  --dataroom "$DR" \
  --require-email \
  --watermark "$RECIPIENT · {{timestamp}}" \
  --json | jq -r '.data.url'

The CLI handles auth, retries, and the supported-file-type filtering internally. The trade-off is the Node startup cost (~80-150ms per CLI invocation), which adds up across many uploads but is invisible for one-shot scripts.

TypeScript with the SDK

For more elaborate workflows. Progress bars, parallel uploads, retry on flaky networks, structured error handling. Use the SDK:

#!/usr/bin/env -S npx tsx
import { Papermark } from "@papermark/sdk";
import { readdir } from "node:fs/promises";
import { createReadStream } from "node:fs";
import path from "node:path";

const [name, dir, recipient] = process.argv.slice(2);
if (!name || !dir || !recipient) {
  console.error(
    "usage: tsx share.ts 'dataroom name' ./folder recipient@example.com",
  );
  process.exit(1);
}

const pm = new Papermark(); // reads PAPERMARK_TOKEN

const SUPPORTED = new Set([".pdf", ".docx", ".pptx", ".xlsx", ".csv", ".txt", ".md"]);

console.log(`→ creating dataroom "${name}"`);
const dataroom = await pm.datarooms.create({ name });
console.log(`  ${dataroom.id}`);

const files = (await readdir(dir)).filter((f) =>
  SUPPORTED.has(path.extname(f).toLowerCase()),
);

console.log(`→ uploading ${files.length} files in parallel (concurrency 4)`);
let done = 0;
await Promise.all(
  files.map(async (f) => {
    await pm.documents.upload({
      file: createReadStream(path.join(dir, f)),
      dataroomId: dataroom.id,
      name: f,
    });
    done++;
    process.stdout.write(`  [${done}/${files.length}] ${f}\n`);
  }),
);

console.log(`→ minting link for ${recipient}`);
const link = await pm.links.create({
  dataroomId: dataroom.id,
  requireEmail: true,
  allowDownload: false,
  watermark: `${recipient} · {{timestamp}}`,
});

console.log(`\n✓ ${link.url}`);

Run it with:

PAPERMARK_TOKEN=pm_live_… npx tsx share.ts "Acme — Series A" ./acme-pack alice@vc.com

Parallel-upload concurrency of 4-8 typically saturates a standard broadband connection without exhausting the API's per-account rate limit.

Python with the SDK

For environments where Node isn't installed but Python is (common in data-science teams and operations):

#!/usr/bin/env python3
import os, sys, glob, time
from papermark import Papermark

def main():
    if len(sys.argv) < 4:
        print("usage: python share.py 'dataroom name' ./folder recipient@example.com")
        sys.exit(1)

    name, directory, recipient = sys.argv[1], sys.argv[2], sys.argv[3]
    pm = Papermark()  # reads PAPERMARK_TOKEN

    SUPPORTED = {".pdf", ".docx", ".pptx", ".xlsx", ".csv", ".txt", ".md"}
    started = time.time()

    print(f"→ creating dataroom {name!r}")
    room = pm.datarooms.create(name=name)
    print(f"  {room.id}")

    files = [
        p for p in glob.glob(f"{directory}/*")
        if os.path.splitext(p)[1].lower() in SUPPORTED
    ]
    print(f"→ uploading {len(files)} files")
    for i, path in enumerate(files, 1):
        with open(path, "rb") as f:
            pm.documents.upload(file=f, dataroom_id=room.id, name=os.path.basename(path))
        print(f"  [{i}/{len(files)}] {os.path.basename(path)}")

    print(f"→ minting link for {recipient}")
    link = pm.links.create(
        dataroom_id=room.id,
        require_email=True,
        allow_download=False,
        watermark=f"{recipient} · {{{{timestamp}}}}",
    )

    elapsed = time.time() - started
    print(f"\n✓ done in {elapsed:.1f}s")
    print(f"  {link.url}")

if __name__ == "__main__":
    main()

Extending the script

Six useful extensions in roughly increasing complexity:

1: add a password from your password manager

PW=$(op item get "Acme dataroom" --field password)
papermark links create --dataroom "$DR" --password "$PW" --json

Works with 1Password CLI (op), Bitwarden CLI (bw), or any other secret-manager CLI. Don't hardcode passwords; don't pipe openssl rand into the script unless you also send the password through a separate channel.

2: send the link via email

RESEND_FROM="deals@yourcompany.com"
curl -X POST https://api.resend.com/emails \
  -H "Authorization: Bearer $RESEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"from\": \"$RESEND_FROM\",
    \"to\": \"$RECIPIENT\",
    \"subject\": \"Materials for $NAME\",
    \"html\": \"Here are the materials: <a href='$LINK_URL'>$LINK_URL</a>\"
  }"

Resend, Postmark, SES, Mailgun, or your own SMTP all work. Use a transactional sender, not a marketing platform.

3: per-bidder loop over a CSV

Wrap the link-minting step in a loop over a recipients CSV:

tail -n +2 recipients.csv | while IFS=, read -r NAME EMAIL FUND; do
  URL=$(papermark links create \
    --dataroom "$DR" \
    --require-email \
    --watermark "$NAME · $FUND · {{timestamp}}" \
    --json | jq -r '.data.url')
  echo "$EMAIL,$URL"
done > links.csv

For 30 recipients, this takes about 40-80 seconds end-to-end. See Per-recipient share links for the deeper pattern.

4: add organized folders

If your document set has natural categorization (Financials, Legal, IP), build the folder tree before uploading:

for folder in Financials Legal IP Operations; do
  FID=$(curl -sS -X POST "$API/datarooms/$DR/folders" \
    -H "Authorization: Bearer $PAPERMARK_TOKEN" \
    -d "{\"name\": \"$folder\"}" | jq -r '.data.id')
  # Upload files matching this folder pattern
  for f in "$DIR/$folder"/*; do
    [ -f "$f" ] && papermark documents upload "$f" \
      --dataroom "$DR" --folder "$FID" > /dev/null
  done
done

5: add expiry and download policy from environment

papermark links create \
  --dataroom "$DR" \
  --require-email \
  --expires "${LINK_EXPIRES_AT:-2026-12-31}" \
  --no-download \
  --watermark "$RECIPIENT · {{timestamp}}"

Externalize defaults so the same script handles different deal contexts without code changes.

6: log the run to your CRM

curl -X POST "https://api.hubapi.com/crm/v3/objects/contacts/$CONTACT_ID/notes" \
  -H "Authorization: Bearer $HUBSPOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"properties\": {
      \"hs_note_body\": \"Dataroom $NAME provisioned. Link: $LINK_URL\"
    }
  }"

Now the deal team has a CRM record of every dataroom they've ever sent.

What you didn't have to build

The combined script above is 30-80 lines depending on language. The infrastructure it replaces. Purpose-built sharing UIs, ad-hoc email-attachment workflows, manual CRM logging, custom watermarking, link expiry management. Typically takes 2-6 engineer-weeks to build internally, and breaks at the edges (the email gateway didn't deliver, the watermark library doesn't handle Unicode, the link expiry job stopped firing). The API approach inherits all of that hardened infrastructure for free.

See also

More in Cookbook