@capdaddy/carta-archive
v1.14.0
Published
Get a complete copy of your own company's records out of Carta. Runs on your machine, against your own logged-in session.
Maintainers
Readme
carta-archive
Retrieve a complete, structured copy of your own organisation's Carta records.
Warnings
Run this tool BEFORE you cancel your Carta subscription. Carta destroys account access permanently after offboarding. You get one window. Do not wait.
Check REPORT.md before you cancel anything. Exit code 1 means some items are still in Carta for you to copy by hand. Exit code 2 means the archive is not a complete copy: read the report, fix the failures, and run again.
These endpoints are private and undocumented. Carta can change them at any time. The tool verifies the shape of every response. It records a structured failure when a shape changes. It never guesses.
Start here
Run it, sign in to Carta in the window that opens, done.
You need Node 20 or newer and Google Chrome. npx fetches the tool and the two
libraries it uses; there is nothing for you to install by hand. The tool drives
the Chrome you already have. It downloads no browser.
1. Find your corporation id. Open Carta and look at the address bar:
https://app.carta.com/corporations/3005703/securities/
^^^^^^^ this number2. Run it.
npx @capdaddy/carta-archive --corp 30057033. Sign in to Carta in the window that opens. The tool opens Chrome on your own Carta page and waits, for up to fifteen minutes. Sign in there and leave the window open. The run starts by itself.
You type your password into Carta, never into this tool. There is no cookie to copy and no developer tools to open. Chrome keeps its own profile beside your archive, so a second run is already signed in.
If Carta asks for a quick security check while the run works, the tool opens that page in the window and tells you. Complete the check and the run continues by itself.
4. Read REPORT.md in the archive directory. Exit code 0 means the copy is complete and there is nothing for you to fetch. Anything else means there is, and the report says what.
5. Upload the zip. A complete run packs your archive into a single ready-to-upload file beside the archive directory, and prints its name. Open capdaddy.cc/import/carta-archive, drop that file in, select "Check it first", then "Load it". You do not have to compress anything yourself.
Size is not a limit. A large archive is sent in parts of 8 MB, both by this tool and by the import page. Each part carries its own checksum. A small part is deliberate. On a home connection of about 1 MB/s, a 48 MB part held one request open for more than two minutes, and the connection dropped before the part arrived.
If an upload stops part way, start it again. The tool asks CapDaddy which parts it holds, and it compares the checksum of each one against the file on disk. It sends only the parts that do not match. A repacked archive is a different file, so the tool sends it again in full.
If your connection refuses a request of 8 MB, use --part-bytes with a smaller
number. The tool also lowers the size by itself when a request is refused
because it is too large.
Loading the same archive twice is safe. Documents are matched on their content hash, board consents on their Carta id, and a security whose real agreement is already in place is left alone. A second load reports what was already there and changes nothing.
Let the run deliver it for you
On CapDaddy's Move-from-Carta page, create a one-time upload code and pass it:
npx @capdaddy/carta-archive --corp 3005703 --upload-code <code>A complete run then packs the archive, uploads it, and prints where to read the report. The code works once, expires in 24 hours, and can only add records to the company it was created for. A large archive travels in parts, so there is no size at which this stops working, and the archive stays on disk either way.
The run checks the code when it starts. The check also holds the code for this run. If the code has less than six hours left, CapDaddy gives it six hours. A code that works when the run starts can then deliver when the run ends.
If the code is dead, the run tells you at once. In a terminal, the run asks you for a new one. Create a new code on the Move off Carta page and paste it. The run does not show the code on the screen, and it does not write it to the log. Press Enter to continue without a code. The run then collects your archive, but it does not upload it.
The run asks again at the end, after it packs the archive. It asks when the code is dead, and it asks when you gave no code at all. Paste a code to deliver the archive immediately. The run collects nothing again.
A scheduled job or a script has no terminal and cannot answer. Such a run continues and writes your archive to disk. The last screen tells you to create a new code on the Move off Carta page. A code that you already used cannot be used again, so you always need a new one.
You then have two ways to finish:
- Upload the packed file by hand on the import page. This needs nothing from Carta.
- Run the same command again with the new code. The second run reuses what is on disk, the reports it already built included, so it is much shorter. It still reads Carta to take whatever the first run did not. Chrome keeps its profile, so it usually opens already signed in.
Use --no-zip to skip the packing step and keep only the directory. An upload
code cannot deliver anything if you also pass --no-zip, because there is no
packed file to send.
A dry run collects nothing. It never checks the code, and it delivers nothing.
The run says so when you pass --upload-code and --dry-run together.
One code loads one archive
A code is good for one load. A load that finishes spends it.
Read this before you run --only. A run with --only collects the families you
name. If that run delivers the archive, it spends the code. Create a new code
before you run the full collection. If you do not, the full archive is not
delivered, and you must upload the file by hand. The last screen tells you this
after a delivered --only run.
If CapDaddy does not answer the last request
The last request starts the load. If CapDaddy takes the archive and does not answer, the tool cannot know whether the load ran. So it asks the code, because a load that finishes spends the code.
- If the code no longer works, the load probably ran. The tool tells you to open the import page and to look at the most recent run. Look first. If you upload the archive again, you load the same records twice.
- If the code still works, nothing was loaded. The tool tells you to upload the file yourself, and your archive is safe on disk.
Would you rather not do any of that?
Give the whole job to an agent. This package ships a skill, so Claude Code and Codex can read it and drive the migration for you, including your logged-in browser. Ask your agent:
Migrate my company off Carta.Prefer the agent if your Carta holds SAFEs, convertible notes, RSUs, RSAs, SARs, more than one equity plan, or more than one currency. This tool was proven against one organisation that had none of those, and it stops rather than guess. An agent can read a record it does not recognise and carry on.
If Chrome is not installed
Without Chrome the tool cannot open a browser for you, so you supply your Carta session yourself. Read "Advanced fallback: supply the session cookie yourself" below. Everything else about the run is the same.
Your password
This tool never asks for your Carta password, never sends it, and never stores it. You type it into Carta itself, in the browser window on your own computer. This tool never reads your Carta session either: the browser attaches it, and the tool only reads the answers.
What this tool does
This tool retrieves the account owner's own organisation data using the owner's own session. It does what you can already do by hand in the browser. It cannot see anything that you cannot already see on screen.
It collects sixteen families of records, in this order:
| Family | Contents |
| --- | --- |
| securities | Certificates, option grants, warrants, stakeholders, and the document to security join. Also the paper Carta generates for each one: the detail panel, the print view, the vesting schedule, the exercise ledger with its documents, and the ISO 100,000 dollar limit screen |
| stakeholders | The roster, every person's profile and relationship history, the employee equity view, the HRIS reconciliation, and who may see what |
| captable | Share class and stakeholder cap tables, one snapshot per milestone date plus today |
| reports | Every report the Carta Run Reports hub offers, except the two named in "The reports" below. The two reports the CapDaddy import reads are included |
| boardroom | Board consents, consent detail, exhibits, consent archives, the board document tree, board members, meetings, and the approved draft securities |
| plans | Equity plans, the pool ledger, legends, vesting schedules, document sets, performance conditions, and acceleration terms |
| charter | Share classes, the authorized shares history, the charter versions, and every certificate of incorporation |
| compliance | The Carta health checks, 409A status, beneficial ownership (CTA), QSBS, Form 3921, the tax administration, and the jurisdiction rates export |
| fundraising | Financing history, outstanding convertibles, saved models, and tender offers |
| drafts | Draft securities of all nine types, with their full terms |
| dataRooms | Every data room, its directory tree, its files, the people invited to it, the record of who read which document, and the rooms other companies shared with you |
| library | The company documents library, with the category, group and date of each document |
| documents | The company balance sheet and income statement, and the document delivery queue |
| communications | The messages the company sent, your inbox, investor relations, audit confirmations, and the employee hub |
| company | Board members, signatories, roles and permissions, the corporation record, the Carta contract, every settings screen, the payment activity, and the dashboard |
| valuations | The 409A history, every completed 409A report PDF, share class terms, the valuation ledger, and the reports in flight |
Run one family on its own with --only <family>.
Money and share counts stay as text. Carta sends full precision decimal strings,
for example "0.180000000000" and "625". The tool copies them into the
manifest without change. It never converts one to a number.
Three ways to run it
Primary: the tool opens Chrome for you
This is what --transport chrome does, and what auto picks when you give no
cookie. You never touch a credential.
Carta's session cookie is HttpOnly. JavaScript on the page cannot read it. Only
the browser can send it. So the tool opens your own Google Chrome, waits while
you sign in to Carta, and then runs each request inside that signed-in page with
fetch(..., {credentials: 'include'}). The browser attaches the session cookie
itself. This tool never reads it, never stores it, and never sends it anywhere.
npx @capdaddy/carta-archive --corp 3005703What you see:
- A Chrome window opens on your Carta securities page.
- The tool prints "Sign in to Carta in the window that opened" and waits, for up to fifteen minutes.
- You sign in. The run starts by itself and prints its progress.
- If Carta asks for a security check, the tool opens that page in the window and asks you to complete it. The run continues by itself afterwards.
Three details that matter:
- Chrome keeps its own profile in
<out>/chrome-profile. A second run is already signed in. The profile holds your live Carta session, so the tool never puts it in the upload file. Use--browser-profile <dir>to keep it somewhere else. Keep that directory on a local disk. Do not put it in a folder that syncs to a cloud service, and do not put it on a shared drive. The tool makes the directory readable by you only, but a folder that syncs copies the session to every device on the account. The tool refuses a directory that overlaps the archive directory, because the archive is what goes into the upload file. - The window is never hidden. A hidden browser cannot be signed into, and Carta's bot check refuses one. There is no flag to turn the window off.
- This path reads two things the others cannot. Cloudflare refuses some
Carta pages to any cookie, one route at a time, and it does not refuse a real
browser. The beneficial ownership record (CTA) lives on
kyc.app.carta.com, and a browser can navigate to it.
If no Chrome is on the machine, the tool says so, names what to install, and points at the cookie path below. It never falls back to a credential by itself.
An agent drives a browser you are already signed in to
Use this when your Carta holds a record this tool has never seen: an agent can read it and carry on. It is the same in-page fetch, with the agent supplying the browser instead of the tool.
The agent supplies a capability object and calls runArchive directly:
import { runArchive } from './scripts/carta-archive/index'
const code = await runArchive(['--corp', '3005703', '--out', './carta-archive'], {
browser: {
// Run a JavaScript function expression in the logged-in Carta tab.
// Return its JSON result. Use chrome-devtools MCP, Playwright MCP, or the
// Codex equivalent.
evaluate: (source) => driveTheBrowser(source),
// Optional. Write a document straight to disk with the browser's own
// download machinery. Use CDP Page.setDownloadBehavior, or click a
// download-attributed anchor. Return the path the browser wrote.
download: (url, target) => downloadInBrowser(url, target),
},
})Each capability serves a different part of the run:
| Capability | What it enables |
| --- | --- |
| evaluate | Every JSON endpoint, every HTML page, and all enumeration |
| download | Document retrieval only |
Document bytes never travel through the agent. An organisation can hold four hundred documents. Routing those bytes through a language model context would cost far too much. The browser writes each file to disk. The tool then verifies the file on disk: size, content type, and SHA-256.
If you supply evaluate but not download, the run still works. It enumerates
everything and collects every JSON record. Each document then records a failure
that names the missing capability. Add the download hook, then run again.
Two more capabilities are optional, and a capability that offers them does more:
| Capability | What it enables |
| --- | --- |
| clearChallenge | Show a challenged page to the person at the keyboard, wait for them to pass the check, then retry the page once |
| readJson | Read an allow-listed session host an in-page fetch cannot reach, by navigating a tab to it |
The Chrome path implements both. Without them the run still lands, and it says what it could not take.
One record needs readJson. Carta serves the beneficial ownership record (CTA)
from kyc.app.carta.com, which is a different host from app.carta.com. From
inside a Carta page this tool sends same-origin requests only. A capability that
cannot navigate therefore gets a refusal that names the Carta screen. The Chrome
path reads the record, and so does the cookie path, because both treat
app.carta.com and kyc.app.carta.com as the two session hosts. You can also
open Compliance and Tax, Beneficial ownership (CTA) in Carta and copy the record
by hand.
Advanced fallback: supply the session cookie yourself
Use this path when Chrome is not installed on the machine, or in CI, where there is no window for anybody to sign in to.
To copy the cookie header:
- Open Carta in a browser. Sign in.
- Open the developer tools. Select the Network tab.
- Reload the page. Right-click any request to
app.carta.com. - Choose Copy, then Copy as cURL.
- Save the command to a file, or export the cookie as
CARTA_COOKIE.
The cookie file accepts the whole cURL command. The tool reads the cookie and
the browser user agent out of it. The user agent matters: Cloudflare binds its
cf_clearance cookie to the browser that earned it.
Cloudflare decides its bot check one route at a time. One run can therefore take
hundreds of pages and still be challenged on one kind of page. If REPORT.md
names a challenged route, your session is not the problem. Run the tool again
with --transport chrome, which opens your own Chrome and needs no cookie. The
bot check does not challenge it.
A cookie that carries no Carta session is refused before the first request. Read
the exact quoting rules, and the failure that produced them, in cookie.ts.
Never pass the cookie as a command-line value. Shell history keeps it forever.
The tool refuses a --cookie flag for this reason.
CARTA_COOKIE="<the cookie header value>" \
npx tsx scripts/carta-archive/index.ts --corp 3005703
# Or read it from a file.
npx tsx scripts/carta-archive/index.ts --corp 3005703 --cookie-file ./cookie.txtThe cookie never appears in the manifest, the report, the raw dumps, or a log
line. The tool sends it to two hosts and to no others: app.carta.com, and
kyc.app.carta.com, which serves the beneficial ownership record that the Carta
application itself reads from there. It checks every redirect hop again, so a
hop to documents.carta.com, to a content delivery network, or to presigned
Amazon S3 drops the cookie. Those hosts authenticate the signature on the link,
not the session, so they need no cookie. A hop to plain http:// drops the
cookie too, whatever the host. The session travels over https or not at all.
How to run it
# See what a full run would retrieve. Retrieve nothing.
npx tsx scripts/carta-archive/index.ts --corp 3005703 --dry-run
# Run the whole archive.
npx tsx scripts/carta-archive/index.ts --corp 3005703 --out ./carta-archive
# Run one family, inspect the result, then run it again.
npx tsx scripts/carta-archive/index.ts --corp 3005703 --only boardroom
# Print the endpoint map and exit. This contacts nothing.
npx tsx scripts/carta-archive/index.ts --emit-endpoint-mapOptions
| Option | Effect |
| --- | --- |
| --corp <id> | The Carta corporation id. Read it from a Carta URL. Required. |
| --out <dir> | The archive directory. The default is ./carta-archive-<corp>. |
| --cookie-file <path> | A file that holds the cookie. It can hold the whole command from Copy as cURL, a Cookie: header, or the bare value. |
| --base-url <url> | The default is https://app.carta.com. The URL must be https. The tool refuses a plain http base before the run starts. |
| --delay <ms> | The smallest gap between two requests. The default is 900, and the run adds up to another 78 per cent of it at random. --delay 0 turns the pacing off; use that only against a test server. |
| --gentle | Double every pause: the gap between requests, the breath every 30 to 50 requests, and the wait between families. Use it if Carta asks you for a bot check part way through a run. |
| --concurrency <n> | 1 or 2. The default is 1. |
| --only <a,b> | Run only these families. |
| --data-room-root <room=dir> | Give a start directory id for a data room. Use this only if the room page stops carrying its root_documents key. Repeat as needed. |
| --transport <auto\|chrome\|browser\|cookie> | The default is auto: it opens Chrome when you gave no cookie and Chrome is installed, and otherwise reads the cookie you supplied. |
| --browser-profile <dir> | Where Chrome keeps the profile it signs in with. The default is <out>/chrome-profile. It is never packed into the upload. Keep it on a local disk, never in a synced or shared folder. |
| --dry-run | Enumerate and measure. Retrieve nothing. |
| --skip-head | With --dry-run, do not probe file sizes. |
| --fresh | Ignore the checkpoint. Start again. |
| --upload-code <code> | A single-use code from the Move-from-Carta page. A complete run then delivers the archive for you. In a terminal, the run asks for a new code if this one is dead. |
| --report-years <years> | Tax years for the reports Carta generates one year at a time. Give them as 2024,2025,2026. The default is this year, which is what Carta's own report defaults to. |
| --part-bytes <MB> | How much archive one upload request carries. Use 1 to 64. The default is 8. Use a smaller number if your uploads keep stopping. |
| --import-url <url> | The CapDaddy import page. The default is https://capdaddy.cc/import/carta-archive. |
| --no-zip | Do not pack the upload file. The archive directory is written either way. |
| --no-color | Never colour the output. NO_COLOR does the same. |
| --ascii | Draw with plain ASCII characters. |
| --emit-endpoint-map | Print the endpoint map as JSON. Exit. |
Exit codes
| Code | Meaning | | --- | --- | | 0 | Complete. Carta offered nothing this tool left behind. Items the archive holds another way do not change this code. | | 1 | Complete, with items to copy by hand, or with records this tool did not recognise. Read REPORT.md. | | 2 | At least one failure, or one count mismatch. The archive is not complete. | | 3 | The run could not start. |
A skip has two meanings, and only one of them is work:
- Items to copy by hand. Carta still holds the item and this run did not take it. Each one is in REPORT.md, under "Items to copy by hand", with the Carta screen that serves it. These give exit code 1.
- Carta offers, this tool leaves in place. Nothing is missing. The archive holds the same records another way, or Carta holds nothing to take: a ledger print view whose rows are in the manifest and in the ledger report, a draft nobody wrote, the monthly and annual views of figures the quarterly series already carries, a report whose other generation landed, a report Carta refuses this organisation, a report the application itself cannot request. REPORT.md lists every one with its reason. These do not change the exit code.
What lands where
<outdir>/
manifest.json Every object, at full precision, plus the run metadata
checkpoint.json Resume state
endpoint-map.json The endpoints this run used
REPORT.md The human summary: counts, failures, skips
files/<family>/ Every retrieved document
raw/ Each response as Carta returned it, with every signed link cut to `<presigned-query-redacted>`
raw/unrecognized/ Every record the tool could not interpretThe families are the sixteen in the table above, and files/ holds one
directory per family that retrieved a document.
valuations reads two pages that have no JSON endpoint behind them: the 409A
and FMV history at /corporations/{corp}/409A/reports/, and the share-class
terms at /corporations/{corp}/share-classes/manage/. Those terms (par value,
seniority, pari-passu, dividend type) appear in no Carta export.
The path and the column names of both pages were observed live. The markup was not. So the parser matches a table by its HEADER NAMES, and a page it cannot read raises a failure. It never reports zero rows as "this company has none". Confirm the parse against the live page on the first run of a new organisation.
securities archives pages as well as documents. Carta keeps the detail panel
and the print view of a security as HTML only. There is no file to download, and
neither page is in Carta's own offboarding export. The tool saves each one under
files/securities/:
| File | What it is |
| --- | --- |
| <label>-modal.html | The detail panel, with every tab in it: the approvals and their signature ids, the documents, the exercise history, the compliance fields, and the legend |
| <label>-print.html | The document Carta renders for that security. For a certificate this is the certificate itself, front and back |
A signed link inside a saved page is cut to <presigned-query-redacted> before
the page is written, the same as in raw/. The page is the record. The link
would have expired within the hour in any case.
| ledger-print-<kind>.html | The whole ledger of one instrument, from the print link the screen supplies |
Carta builds the whole-ledger print link with the filter query of the list you
are looking at. The link on its own answers HTTP 404. The run records that
answer as a skip and not as a failure, because every row of that ledger is
already in manifest.json and in the Securities Ledger report. To keep the
printed page as well, open Securities in Carta, select the instrument tab, and
use Print.
The tool also reads the same panel into records, under securities.details. Two
rules govern that reading. It keeps every date and every money amount as the
exact text Carta printed. And it fails closed: a shape it cannot read yields no
record at all, the markup stays on disk, and the run says which security it
could not read. A half read exercise row would be worse than no row.
manifest.json holds the archive. Each file entry records the byte count, the
SHA-256 hash, the content type, and the join back to its security, consent, or
data room. REPORT.md prints the expected count against the retrieved count for
every family.
The reports
Carta keeps some of your records in its Run Reports hub only. The hub builds a
spreadsheet on request; no endpoint returns the same data. The tool therefore
generates every report the hub offers, except the two named at the end of this
section. It waits for each report, and it downloads the report into
files/reports/.
The tool records each report under a fixed name: cap-table.xlsx and ocx.xlsx
for the two reports the CapDaddy import is built on, and the Carta report type
for every other one, for example stakeholder_contact_report.xlsx.
Those are the names in manifest.json, not the names on disk. Each file lands as
files/reports/<report id>-<filename>. The report id is the id Carta gave the
generation. The filename is the name Carta sent with the download, or the fixed
name above when Carta sent none. So a real run writes
files/reports/11082423-HANK.ai-s-Cap-Table.xlsx, or
files/reports/11082423-cap-table.xlsx. Do not look for
files/reports/cap-table.xlsx. No run writes that path.
Read manifest.json to find a report. Each entry under files records name
(the fixed name above), path (where the file really is), and a join that
names the report type and the parameters the run sent. The CapDaddy import reads
those entries, never the directory listing.
| Report | Hub folder | What it carries | | --- | --- | --- | | Cap Table | Capitalization | The capitalization table, with the Detailed worksheet and the securities ledgers by type and class. The CapDaddy import reads this file first. | | OCX | Capitalization | Equity data in the Open Cap Table Data Format. The CapDaddy import reconciles it against the cap table report at the same as of date. | | Stakeholder Ownership Details | Capitalization | Ownership for each stakeholder, at the as of date. | | Canceled and Returned Report | Securities | Securities that were canceled, and the shares that returned to the pool. | | Certificates Ledger | Securities | The ledger of certificates. | | Equity Plan Granted | Securities | Everything granted under the equity plans. | | Mass Issuance Report | Securities | Securities issued in bulk, at the as of date. | | Options Outstanding Report | Securities | Every option that is still outstanding. | | Securities Ledger | Securities | The ledger of every security. | | Share Registry | Securities | The share registry. | | All Stakeholders Ledger | Stakeholder | The ledger of every stakeholder. | | Stakeholder Details | Stakeholder | The full detail Carta holds for each stakeholder. | | Equity Awards Outstanding | Equity Plan | Every equity award that is still outstanding, across the plans. | | Equity Plan | Equity Plan | Equity plan activity at the as of date, the summary worksheet only. Carta leaves the transactions ledger, the rollforward, and the stakeholder sums worksheets unticked, and this run sends those defaults. Generate the report from the Carta hub with those boxes ticked if you need them. | | Equity Pool Transaction Ledger | Equity Plan | The equity pool values with every transaction that moved them, in summary and in detail. | | Form 1099b Cost Basis Report | Equity Plan | The cost basis and the acquisition dates behind the Form 1099-B for a tender offer, for one tax year. | | Revenue Procedure Disclosure Statements Report | Equity Plan | The revenue procedure disclosure statements for one tax year. | | Tax Withholding Report | Equity Plan | The tax withholding events Carta processed, over a period. | | 83(b) Elections | Compliance | The 83(b) election filing status of every eligible security. | | Disqualifying Dispositions Report (ISO) | Compliance | Disqualifying dispositions on certificates that came from an incentive stock option, between the earliest and the latest transaction date. | | Documents Report | Compliance | Every security with its id, its holder, and the documents attached to it. | | Rule 701 Analysis | Compliance | The Rule 701 exemption analysis over a period. An empty from date means one year. | | Security Acceptance Status Report | Compliance | Every security and whether its holder has accepted it. | | Stakeholder Contact Spreadsheet | Compliance | Every stakeholder with their name, email address, and postal address. | | Transactions Audit Report | Compliance | Every action taken in the Carta account, over a period. | | YTD Payroll Records Report | Compliance | The compensation values uploaded for tax withholding, over a period. | | Certificate Transaction Report | Transactions | Every transaction against a certificate, up to the as of date. | | Exercised and Settled | Transactions | Every option exercise and every settlement. | | ISO/NSO Vesting Report | Vesting | Vesting tranche by tranche, with the incentive and non qualified split. | | Vesting Details | Vesting | Vesting tranche by tranche. | | Forfeiture Report | Terminations | An estimate of the forfeiture rate, from the terminations already recorded. | | Historical Terminations | Terminations | What each recorded termination did to the equity plan. | | Tender Offer Seller Model | Modeling | Eligibility and transaction parameters for each stakeholder in a Carta Liquidity transaction. | | Voting Rights Report | Modeling | Outstanding ownership and votes for every stakeholder. |
The run also asks for the reports Carta keeps for a public company, for an employee stock purchase plan, and for the older version of a report it now serves a newer one of. Your organisation probably has none of these. Carta answers each one with a refusal, and the run records that refusal as a skip, because nothing is missing from your archive. An organisation that does hold those products gets the reports.
Carta refuses such a report in more than one way. It answers with an error status, or it accepts the request and then marks the finished report with an error. Both are refusals, and the run records a skip for both. The one answer the run never reads as a refusal is HTTP 401, because that means your session has ended.
Carta serves some reports by two paths at once: an older path and a newer one. The two produce the same report, and the hub shows one row. The run asks for both. If one of them produces the file and Carta refuses the other, the run records a skip that names the report it does hold. Your archive has the report.
Two reports the hub offers are NOT generated:
- The Carta SOC report. The download signs a non disclosure agreement, which is a legal act by a person and not a copy of your records. Sign it and download the report from the hub if you need it.
- Termination modeling. The report models a termination that has not happened, so it needs a stakeholder, a date, and a reason that you choose. It holds no record of its own.
manifest.json carries the whole catalogue under reports.catalogue, with the
reason beside every report the run did not generate, and one row per request
under reports.generated.
Two reports are built one tax year at a time: the Form 1099b cost basis report
and the revenue procedure disclosure statements report. Carta draws that year
list in your browser and serves it from no endpoint, so the run takes the year
Carta itself selects, which is this one, and names the earlier years as a skip.
Read the Year list on the report and pass --report-years 2024,2025,2026 to
take them.
The tool is polite
Requests run one at a time by default. The limit is two at a time. There is no burst mode. The run is paced like a person reading their own records, not like a script:
| What | How long | | --- | --- | | The gap between two requests | 900 to 1600 ms, drawn at random for each one | | A breath, every 30 to 50 requests | 3 to 6 seconds | | Between two families | 5 to 10 seconds | | The rate, at most | 40 requests a minute, whatever the gaps come out at |
The gap is drawn at random on purpose. A fixed gap is its own signature: nothing
a person does arrives every 750 ms to the millisecond. The rate ceiling is
separate from the gap, because a floor on the spacing is not a ceiling on the
rate. The run prints the ceiling once for each family, as
pacing: human, 40 requests per minute at most.
The run slows itself down when Carta asks it to. A 429, a 5xx, or a Cloudflare
check doubles the gap for the rest of the run, up to 8 seconds. The run says so
once. Retry-After is honoured as it always was.
--gentle doubles every pause above: the gap, the breath, and the wait between
families. Use it if Carta asks you for a bot check part way through a run.
--delay <ms> sets the smallest gap, and the run still adds up to another 78 per
cent of it at random. --delay 0 turns the pacing off; use that only against a
test server.
The tool retries a network fault, a 408, a 425, a 429, and a 5xx. It honours
Retry-After. It does not retry a 401, a 403, or a 404, because those are
answers and not faults.
What the tool does NOT do
This is the whole posture, and it is short on purpose:
- It does not spoof a fingerprint. It does not pretend to be a browser it is not, and it does not forge a device, a screen, a font list, or a canvas.
- It hides nothing beyond the two launch flags it already uses, which suppress Chrome's automation banner over the window you type your password into.
- It uses no proxy, no address rotation, and no third party network.
- It solves no bot check. When Carta or Cloudflare asks for a human check, the window comes forward and you answer it yourself.
The tool is your own installed Chrome, with your own session, reading your own records at a human pace. Nothing about it is designed to look like anything else.
The run survives what happens on a laptop
A migration runs for the better part of an hour on a machine you are also using. Three things go wrong in that hour, and the run handles all three.
Carta signs you out. The run notices, checks the session from inside the
page, and pauses. The window comes forward with Carta's sign-in page on it and
prints Carta signed you out. Sign in again in the window; the run continues by
itself. Sign in, and the run continues with the record it stopped on. Nothing is
recorded as a failure because of it. If nobody signs in within 15 minutes, the
run stops early with one reason, keeps everything it has, and tells you the
command to run again.
You close the window, or Chrome stops. The run finishes the record it is on,
writes the checkpoint, and prints The browser window was closed. Run the same
command again to continue where this stopped. It exits with code 2, because the
archive is genuinely short.
The network drops. One lost request is a blip and is recorded as any other
failure. Five in a row is an outage: the run pauses, re-checks the connection
every 30 seconds for up to 10 minutes, prints waiting for the network, and then
carries on with the record it stopped on. If the connection never comes back,
the run stops early with one reason.
A laptop that slept in the middle of a run wakes up in one of those states, so the cases above cover it. In every case the run stops with ONE reason rather than turning every remaining record into its own failure, and everything already retrieved is kept.
The run is safe to interrupt
The tool writes a checkpoint after every completed unit of work. Stop the run at
any time. Start it again with the same --out directory. It continues from the
checkpoint. It reads each earlier response from raw/ instead of asking Carta
again, with three exceptions.
A response that carried a signed link is read from Carta again. The copy in
raw/ has the signature cut out, so it cannot serve a download. The tool sees
the cut and asks Carta for the response again, follows the fresh link at once,
and never stores or reuses a signed link. Board consent documents, charter
filings, plan documents, 409A reports, warrant documents and the security detail
panels all work this way. A response that still holds a signature is read again
too. A stored signature has been expiring since the moment it landed, so the
tool never follows one.
A file the manifest records is trusted only when its bytes are still on disk. If a file went missing between two runs, the tool retrieves it again rather than report a complete archive over a hole.
A checkpoint speaks only for the version that wrote it
The checkpoint records the version of this tool that made it. A run that finds a checkpoint from a different version does not continue from it. It starts again and asks Carta for every list, because a saved answer from an older build can be weeks out of date, and a security issued since then would be missing from your archive with nothing to tell you. The run prints one line that names the version that wrote the checkpoint and the date.
The run keeps every document already in the archive. It matches a document by
its SHA-256 hash, so it stores identical bytes once and never writes a second
copy. It moves the earlier raw/ directory to raw.<version>/ so the new run
cannot mix its answers with the old ones. Nothing is deleted.
Nothing is dropped
Some record shapes are not validated, because our own organisation does not have them. We have no SAFEs. We have no convertible notes with balances. We have no RSUs, RSAs, or SARs. We have one equity plan, one currency, and one jurisdiction.
When the tool meets a record it cannot interpret, it does three things:
- It writes the record to
raw/unrecognized/, exactly as Carta sent it. - It lists the record in
manifest.json, underunrecognized. - It continues with the rest of the run.
Losing a record is the one unacceptable outcome. An unrecognised record forces exit code 1, so you cannot miss it.
The tool also records every item it did NOT take, and it says which of the two meanings applies. An item you must copy by hand forces exit code 1 and is named on the closing screen. An item the archive already holds another way does not change the exit code. REPORT.md lists both, in two sections, so you can see every decision this tool made for you.
Failures are machine readable
Each failure in manifest.json carries these fields:
| Field | Contents |
| --- | --- |
| family, unit | Which collector failed, and on which unit of work |
| httpStatus | The status Carta returned, or null for a transport fault |
| endpointId, endpoint | The endpoint map entry, and the path that failed |
| expectedKeys, receivedKeys | The keys required, and the keys received |
| excerpt, rawPath | A short body excerpt, and the full body on disk |
| remediation | The Carta page or report that supplies the same data |
REPORT.md prints the same object in a readable form. Only the session cookie
is removed from a failure body. Presigned signatures are also removed, because a
signature is a credential and it stops working within an hour.
How to re-discover a changed endpoint
Carta can change a private endpoint at any time. Use this method, and only this method:
- Open the real Carta page in a browser. Sign in.
- Open the developer tools. Select the Network tab.
- Filter the requests to XHR and fetch.
- Use the page as normal. Watch the calls that the application itself makes.
- Record the path, the query parameters, and the response keys.
- Update the entry in
scripts/carta-archive/endpoints.ts. SetverifiedOnto today.
Run --emit-endpoint-map first. It gives you the old path, the expected
response keys, and the date somebody last verified the call.
Do not guess a URL. Do not bulk-probe URLs. Guessing is unreliable, it produces wrong data quietly, and it looks like scanning. Record the call that the application makes instead.
What this tool cannot reach
The tool does not collect the records below. Retrieve each one by hand before you cancel.
| Record | Why | Manual fallback |
| --- | --- | --- |
| The equity totals of a stakeholder who is not an employee | Carta serves the employee view filtered to its own six employment relationships. This tool does not guess an unfiltered request. The person is still in the roster and in the full stakeholder records. | Stakeholders. Open the person. Read the Holdings tab. |
| The Carta invoice history | The invoices are held by YayPay, outside Carta. The tool records the link and does not follow it. | Company settings, Subscription details. Open the statement link. |
| The administrator's own account record in the manifest, beyond the identifying fields | The account belongs to the person, not to the company, so the manifest keeps the name and email only. The full response is still in raw/company.me.json. | The account menu, User settings. |
| The company account users who can reach a data room | A room's Access tab counts them beside the invited people, but the endpoint answers with the invited people only. The account users come from the company permissions screens, which the company family reads. | Documents, Data rooms. Open the room, Access tab, and select View account users. |
| A file attached to a message in the communication centre | No message on the validated organisation carried one, so the shape of an attachment link was never observed, and this tool never builds a document URL. A message that says it has attachments is reported, never dropped. | Communications. Open the message and download each attachment. |
| The second and later pages of your inbox | The inbox lists send their page number in a parameter that was never observed. One page is read, and a run that finds more pages says so. | The account menu, Inbox. Read the remaining pages there. |
| The detail of a saved round model, a pro forma model, or a tender offer | The list is collected. The detail view was never observed live, and this tool does not guess an endpoint. | Scenario modeling. Liquidity, Tender offers. Open each one. |
| A document attachment with no link in the payload | The tool never builds a document URL. | Open the security. Use its Documents tab. Or run the Documents report. |
| The detail panel of a convertible note or a SAFE | The organisation this tool was proved against holds none, so the panel was never opened. The tool asks for it on the path the application's own code builds, and it keeps whatever comes back as an unrecognised record if the answer differs. | Securities, SAFEs and convertibles. Open each one and copy its tabs. |
| The Carta SOC report | The download signs a non disclosure agreement. That is a legal act by a person, not a copy of your records. | Run reports, Carta SOC Report. Sign the agreement and download it. |
| The termination modeling report | It models a termination that has not happened, so it needs a stakeholder, a date, and a reason that you choose. | Run reports, Termination Modeling. Choose the stakeholder and the date. |
| Earlier tax years of the Form 1099b and the revenue procedure reports | Carta draws the year list in your browser and serves it from no endpoint, so the run takes the year Carta selects. | Read the Year list on the report, then run again with --report-years. |
REPORT.md does not name every row of this table. Read the three groups below
before you trust the exit code.
The run records a skip. It names these rows on every run that reaches the screen behind them: the equity totals of a stakeholder who is not an employee, the administrator's own account settings, the detail of a saved round model, a pro forma model or a tender offer, a document attachment that carries no link, the Carta SOC report, the termination modeling report, and the earlier tax years of the two reports Carta builds one year at a time. A skip holds the archive short of complete, and the run then exits non-zero.
The run records a failure or an unrecognised record, and only when your archive holds the case. A file attached to a message in the communication centre is a failure. The second and later pages of your inbox are a failure. The detail panel of a convertible note or a SAFE is an unrecognised record, when the answer differs from the shape the walk observed. A company with no message attachment, a one page inbox and no convertible note gets none of these records, because there is nothing to record.
The run names nothing at all. Two rows live in this table only: the Carta invoice history, and the company account users who can reach a data room. A run that retrieves everything else reports the archive complete and exits zero while these two rows are uncopied. Copy these two by hand from this table.
The tool now DOES collect the records this table used to list. It collects every stakeholder profile, with the date of birth, the obfuscated tax id, the payroll id, and the dated history of every relationship change. It collects every company settings screen, including the two that Carta serves as forms with no data endpoint: it saves each page and reads the current value of every field out of it. It also collects the 409A and FMV history, the share class terms, the authorized shares history, every certificate of incorporation, the vesting schedules, the legends, the document sets, the performance conditions, the financing history, and the draft securities.
It also collects the whole approval trail of every security, with the date and the signature id of each approval, plus the exercise ledger, the 83(b) filing state and the withholding record behind each exercise, the four documents Carta keeps for each exercise, and the ISO 100,000 dollar limit screen.
Four links on an exercise request are NEVER called. One sends a reminder email to a stakeholder. The other three approve, reject, or adjust the exercise. This tool reads a ledger. It changes nothing.
It now also collects the data room access lists and read histories, the company document library categories, the company financials, the document delivery queue, the communication centre messages with their bodies, your inbox, investor relations, the audit confirmations, and the employee hub.
The tool does collect board consent documents. Carta keeps the download link out of the consent record. A second endpoint mints a presigned link for the consent file, and the tool uses that link immediately. If the mint call fails, the tool records a failure for that consent. Then open Board, Consents in Carta and save the PDF. Or download the Consents folder as a zip.
These conditions apply to the data rooms:
- A data room has no single root directory. The room page is client rendered, so
the served page holds no table. It carries the top level listing as HTML
entity encoded JSON, under the key
root_documents. The tool reads that key and decodes it. Each entry is a complete node, so the tool knows which entries are directories and which are files. A top level entry that is a file is downloaded directly. - If Carta removes the
root_documentskey, the tool can find no start point. It then fails that room and tells you what to do. Open the room. Open the developer tools. Read a directory id from the/api/dirs/<roomId>/<dirId>request. Run again with--only dataRooms --data-room-root <roomId>=<dirId>. - The completeness check for a room is the number of files the tool enumerated
against the
document_countCarta publishes for that room.REPORT.mdprints both numbers for every room. A difference is a failure, and it sets exit code 2. The number of top level entries is never a completeness signal: one room holds 144 files under a single top level folder. - Carta stamps some data room files with a watermark. The archived bytes then
differ from the original upload. The manifest records
watermarkedfor each such file.
These conditions apply to the security attachments:
- Each attachment is
{id, url, name}. The field isurl. The value is an absolute URL ondocuments.carta.com. That host is notapp.carta.com, and the URL carries its own access token in the path. - The tool never sends the Carta session cookie to that host.
- The tool never fetches those bytes from inside the Carta page. The browser blocks a cross origin fetch. The bytes go through the browser download path. If the agent supplies no download capability, the tool records a failure for that attachment and names the manual fallback.
- Several securities can share one attachment, with the same URL and the same filename. The tool keys each stored file by the attachment id, and it de-duplicates identical content by SHA-256 hash. Two documents with different content never share a path, and one file never overwrites another.
Tests
npx vitest run tests/carta-archive/The tests use fixtures and a stubbed fetch. They never touch the network.
After the archive: load it
An archive is not a migration. scripts/carta-load/ moves it into CapDaddy and
tells you what is still missing.
npm run carta:plan -- --archive <dir>
npm run carta:load -- --archive <dir> --org <slug>
npm run carta:verify -- --org <slug> --archive <dir>A migration is finished when carta:verify prints placeholders: 0. See
scripts/carta-load/README.md.
