# Invisible Cap Table — Product Guide ## What is Invisible Cap Table? Invisible Cap Table is an AI-native cap table management system. Unlike traditional cap table software with dashboards, forms, and complex menus, Invisible is a pure chat interface. You talk to it in plain English to manage your company's equity — view ownership, record transactions, manage option grants, invite stakeholders, and prepare for Board meetings. There is no UI to learn. Just describe what you need, and the AI handles the rest. ## The Invisible products Invisible is a family of products that share one login and one organization structure: - **Invisible Cap Table** at **cap.invisible.app**: everything in this guide about stakeholders, grants, transactions, SBC and 409A. - **Invisible Sign** at **sign.invisible.app**: e-signature with no editor (see "Invisible Sign (E-Signature)" below). Its assistant only does signing, and it's available to any organization, with or without a cap table. - **invisible.app** is the front door: signed out, it introduces both products; signed in, it takes you back to the one you used last. Signing in on one signs you in on all of them. An organization lists the products it uses: one created at sign.invisible.app starts with Sign only, and using another product turns it on. Links in emails (invites, signing links, grants) keep working on invisible.app. ## Who is it for? - **Founders & CEOs** — Full visibility and control over your cap table - **CFOs & Finance Teams** — Record transactions, manage equity plans, prepare Board materials - **Investors** — View your holdings and cap table position across portfolio companies - **Employees** — See your equity grants, vesting schedules, and exercise options - **Lawyers & Accountants** — Read-only access to cap table data for audits and compliance ## How it works 1. **Sign up** — Create an account at invisible.app. You'll get a branded verification email from `noreply@invisible.app` with a 6-digit code; paste it into the chat to confirm. If the email doesn't arrive, type **resend** in the chat to get a new one. Existing users who never confirmed can re-enter their email/password on login — the chat will automatically prompt for the code instead of failing. 2. **Create an organization** — Set up your company 3. **Load your cap table** — Tell the assistant about your stakeholders, funding rounds, and option grants. You can describe them conversationally or upload a spreadsheet. 4. **Manage everything through chat** — Ask questions, record new transactions, generate reports, invite team members. ## Capabilities ### View & Analyze - **Full cap table** — See all stakeholders, share classes, ownership percentages, fully diluted breakdown - **Stakeholder detail** — Holdings, transaction history, option grants for any stakeholder - **Option grants** — Vesting status, exercise prices, expiration dates, with as-of-date calculations - **Security classes** — All preferred series with economic terms (OIP, liquidation preferences, conversion ratios, seniority) - **Transaction history** — Complete ledger of all equity transactions with filters ### Manage Equity - **Record transactions** — Issuances, transfers, exercises, cancellations, conversions - **Manage option grants** — Create grants with vesting schedules, track through approval workflow (proposed → pending approval → approved → active) - **Grant categories** — Tag grants as new hire, promotion, retention, performance, refresh, or advisory - **Update terms** — Modify conversion ratios (for anti-dilution ratchets), authorized shares, OIP - **Equity plans** — Create and manage incentive plans with authorized share pools - **Grant acceptance** — Employees accept (or decline) their stock option grants directly in the chat. The assistant presents the full terms — shares, exercise price, vesting, expiration, the plan terms, and any grant-specific terms — and the employee types their full legal name to affirm. Acceptance is stored as an immutable consent record: a frozen snapshot of the exact terms, the affirmation name, timestamp, and IP, so it can't drift if the grant is later edited. A PDF grant agreement can be generated on demand from that record. - **E-signature (Invisible Sign)** — Send a stock option grant agreement for real e-signature instead of typed-name acceptance. Invisible generates the agreement from the grant and places every signature field itself, so there is nothing to drag or position. If the org has an option signatory (usually the CEO) they countersign first, then the optionee signs; each gets an email link and signs on a simple web page, with no Invisible account needed. When the last person signs, the document gets a certificate of completion and a tamper-evident digital seal, is stored where it cannot be altered, the grant is marked accepted, and everyone gets a copy. See **Invisible Sign** below. - **Stock certificates** — When an investor asks for a stock certificate, issue one for any holding of common or preferred stock. Invisible generates a traditional certificate (certificate number, holder, shares in words and figures, class, par value, state of incorporation, issue date, and restrictive legends on the reverse), two company officers sign it in order, and the holder is emailed the sealed certificate. See **Invisible Sign** below. - **Option exercise** — Employees exercise their vested options through a signed Notice of Exercise, and admins record it in the cap table. The employee reviews how many vested shares they can exercise (vested minus already-exercised minus any pending notice), types their full legal name to affirm, and submits — creating an immutable, signed record and a downloadable Notice of Exercise PDF. Submitting does **not** change holdings: payment (shares × strike) happens outside Invisible, and an admin records the exercise once funds are received, which issues the shares as common stock and turns the employee's options into shares they own. Admins can reject/cancel a pending notice, and employees can withdraw their own before it's processed. Early exercise (exercising unvested shares) is not supported yet. - **Plan & grant terms** — Admins provide the terms employees review, as markdown on the equity plan (shared by its grants) and optionally per grant. Terms can be pasted, drafted with the assistant, or — over MCP in Claude — converted straight from a Word/PDF plan document. - **Beneficial ownership** — Track who owns economic interests in entity stakeholders (e.g., "Grant Kitching owns 3% of Real Magic LLC"). The system computes equivalent shares from the entity's holdings. - **Display names** — Set a common/friendly name on entities with obscure legal names (e.g., "Levitate Employee Fund" for "LE-0210 Fund I, a series of Roll Up Vehicles for LP") - **Cancellations & exercises** — Cancel a whole grant (returns its pool) or just part of one with `cancel_grant_shares` (forfeit an unvested remainder on termination; only those shares return to the pool, and they come off the grant's unvested count). If that leaves nothing outstanding on a grant that was partly exercised, the grant ends as exercised and its exercised shares stay counted as used from the pool. Record a past exercise whose shares are already in the ledger with `record_historical_exercise` — it logs the exercise and links the existing certificate without issuing new shares (use this for migrations instead of the live exercise flow, which issues shares). Repurchases and cancellations can be recorded against an existing certificate; the ledger keeps the reference and gives the new entry its own id. Grant-status changes accept an effective date (e.g. a termination date). The pool nets out cancelled shares (returned) and keeps exercised shares consumed; `get_cap_table_data` (view `equity_plans`) now reports authorized, outstanding, exercised, and available for each plan. - **Terminations & exercise windows** — Record a departure with `record_termination`: it cancels the unvested remainder of each outstanding option grant (returned to the pool) and sets the post-termination exercise deadline. That deadline is an explicit date, or the termination date plus a window: a per-grant override, or the plan's default (usually 90 days; set with `update_equity_plan`). Vested options stay exercisable until the deadline and then lapse automatically: they stop counting as outstanding and return to the pool. The same applies when an option reaches its normal expiration date. Deadlines are computed from the stored dates whenever the cap table is read, so correcting a date fixes the numbers immediately. `get_grant_data` (view `exercise_deadlines`) shows what's coming up and what has lapsed, with the shares at stake. It flags ISOs whose window runs past 3 months after termination, since shares exercised after that lose ISO tax treatment. Grant listings and cap table downloads show the termination date, exercise deadline, and lapsed status. Re-run `record_termination` to change a window (e.g. a negotiated extension). RSAs are skipped; record their repurchase separately. - **Merge stakeholders** — Consolidate duplicate stakeholder records. Moves all grants, transactions, and beneficial ownership from source to target, then deletes the duplicate. Admin only. - **Update stakeholders** — Modify name, email, department, job title, display name, employee ID, or manager on existing stakeholders. Useful for HR syncs. ### Organize & Report - **Investor groups** — Group stakeholders for Board reporting rollups - **Security class summaries** — Breakdown by class with economic terms - **Ownership analysis** — Fully diluted ownership with as-converted calculations ### Team Management - **Invite team members** — Add admins, finance team, lawyers, and investors with appropriate access levels. Invites can include embedded permissions (department access, full cap table visibility) that are applied automatically on acceptance — no manual setup after someone joins. - **Invite stakeholders** — Invite investors and employees to view their own holdings - **Link one person to multiple entities** — A single account can be linked to several stakeholder records in the same organization, so a VC who holds through more than one fund sees all of their funds under one login. Attach every entity in a single invite, or link additional entities (e.g. a newly added fund) to someone who is already in — no re-invite needed. - **Beneficial ownership** — Track who sits behind an entity on the cap table, so equity held through an LLC or fund is attributed to the people who actually own it. Asking what someone owns returns their direct holdings and their indirect interests together; neither is reported alone. - **Generate spreadsheets & PDFs** — Ask for any data or analysis as a downloadable file. "Give me my equity as a spreadsheet", "export all option grants to Excel", or "a PDF of my proceeds if the stock hits $50/share" all produce a file with a download button. The assistant composes small or derived content itself; whole datasets (the full cap table, all grants) are built server-side. Files only ever contain data you're allowed to see. - **Cognito user management** — Admins can list Cognito users with verification status, force-confirm unverified users, or resend verification emails. Useful when new users have trouble with email verification. - **Role-based access** — Owners and Admins can manage everything; Viewers see the cap table; Employees see only their own grants ### Support & feedback - **Report a bug** — Anyone can file a bug report from the chat or MCP (`report_bug`). It notifies the Invisible team and helps us improve; you may not get a direct reply. - **Request support** — File a support request (`request_support`) with a question or something you need help with. The team is notified and replies by email, and the reply also lands in your notifications. - **Notifications** — `get_account_data` (view `notifications`) shows your Invisible notifications (like replies to your requests). Your assistant can check it occasionally so you see replies and updates without leaving the chat. ### Visibility & Org Hierarchy - **Manager hierarchy** — Set reporting relationships between stakeholders. Managers automatically see equity data for their full report tree (direct and indirect reports). - **Department-based access** — Grant users view access to all equity data in specific departments, without being in the manager chain. Perfect for RevOps, HR, or finance roles. - **Granular middle ground** — Between "own holdings only" and "full cap table" access, managers and department-access users see exactly the subset they need. - **Bulk org chart import** — Set manager relationships in bulk via AI, including integration with HR tools like Levitate. ## Common Workflows ### "I just closed a funding round" 1. Tell the assistant about the new series (name, price per share, authorized shares, terms) 2. List the investors and their share amounts 3. The assistant creates the security class, stakeholders, and records the issuance transactions 4. View the updated cap table to confirm ### "I need to issue new option grants" 1. Tell the assistant who's getting grants, how many shares, exercise price, and vesting terms 2. The assistant creates the grants in "proposed" status 3. Review the grants and approve them (or present to the Board first) 4. Once approved, update status to "active" ### "I want to invite an investor to see their holdings" 1. Ask the assistant to invite the stakeholder by email 2. Invisible emails them an invitation with a single link 3. They open the link, see who invited them and to what, choose a password, and confirm with a 6-digit code emailed to them 4. They land in their holdings, now linked to their account The recipient's email address is fixed by the invitation, and they confirm it with a code sent to that inbox — which is what proves the person accepting is the intended recipient, not someone who merely came across the invite link. If they already have an Invisible account, the same link asks for their existing password and accepting takes one click. An invitation can only be accepted by someone signed in as the invited email address. ### "One of my investors has several funds" Many VCs invest through more than one fund, and each fund is its own entity on the cap table. You don't have to send a separate invite per fund: 1. Ask the assistant to invite the investor and name all of their funds (e.g. "invite jane@fund.com and link her to Acme Fund II, Acme Fund III, and Acme Fund IV"). One email, one link, and accepting links all of the entities to her account. 2. If a fund is added later, ask to link it to her existing account by email — she doesn't need to accept anything again. 3. When she signs in, she sees the holdings of every linked entity, grouped by fund. Linking only ever claims entities that aren't already tied to someone else, so you can't accidentally hand one investor's fund to another person. You can also unlink an entity from an account if something was linked by mistake. ### "I want this as a spreadsheet or PDF" 1. Ask for it in plain language: "export all option grants to Excel", "give me my holdings as a CSV", or "make a PDF showing my proceeds at $50/share" 2. The assistant generates the file and shows a **Download** button right in the chat 3. Small or custom content (one person's holdings, a scenario analysis) is composed on the fly; entire datasets are built server-side so nothing is lost or truncated Available file tools: `create_spreadsheet` (XLSX/CSV you compose), `create_pdf` (documents/reports), and `export_data` (whole datasets: cap table, option grants, stakeholders, security classes). Through MCP, the same tools return the file content for your client to save. ### "I need to prepare for a Board meeting" 1. Ask for a cap table summary 2. Ask about the option pool — how many shares authorized, outstanding, available 3. Review any pending option grants that need Board approval 4. Ask for an ownership breakdown by investor group ### "Our latest round had anti-dilution protection" 1. Tell the assistant the new conversion ratio for the affected series 2. The assistant updates the security class 3. View the cap table to see the as-converted impact ### "Employees need to accept their grants" 1. (Optional, once) Admin sets the plan terms employees will review: "Set the terms for our 2019 plan to …" — or, in Claude over MCP, "here's our plan PDF, use it as the plan terms" 2. Admin reviews who's outstanding: "Who hasn't accepted their grants yet?" 3. Admin sends notifications: "Notify everyone with pending grants." This is smart about account status: - Recipients who already have an account get a "Review & accept your grants" email linking to their grants page. - Recipients who don't have an account yet get a setup invitation instead — its link creates and links their account, then lands them on their grants (a plain nudge would otherwise dead-end at signup). - Recipients with no email on file are skipped and reported back to the admin by name. 4. Each recipient lands on a grants page showing every grant's full terms (numbers, plan terms, any grant-specific terms) with an explicit affirmation: they type their full legal name to accept, or decline. 5. Acceptance is recorded as an immutable consent snapshot; the employee (or an admin) can download the PDF grant agreement any time. 6. (Optional) Turn on automatic reminders: "Remind employees who haven't accepted their grants every week." Off by default; when on, each person with grants waiting is reminded weekly, up to 3 times, and the count starts over if they get a new grant. A manual nudge counts too, so an automatic one never follows right behind it. Employees can reach this page any time at **invisible.app/grants**. ### "I want grant agreements signed electronically" 1. (Once) Choose who countersigns for the company: "Make me the option signatory, title Chief Executive Officer." Skip this and agreements go to the optionee alone. 2. Send it: "Send Priya her grant agreement to sign." The assistant shows a download of the generated agreement as a preview. 3. The signatory gets an email, reviews the agreement, agrees to sign electronically, adopts a signature (drawn or typed), and signs. The optionee is emailed next and does the same. 4. Check progress any time: "Who hasn't signed the grant agreements?" The assistant can resend a link (old links stop working) or void a request to fix a mistake. 5. When the optionee signs, the agreement is sealed, the grant is recorded as accepted (source "Invisible Sign"), and everyone is emailed a link to the signed copy. Admins can download it: "Download the signed agreement for grant 42." ### "Draft an NDA and send it for signature" 1. Describe it: "Draft a mutual NDA between Real Magic and Northwind Analytics, two-year term, North Carolina law. I'll sign for Real Magic as CEO." The assistant asks for anything essential that's missing (party names, key terms, governing state), then writes the full agreement. 2. It shows a PDF preview. Every signature, name, title and date line is already placed for each party. Nothing has been sent. 3. Revise by chat: "Make the term three years and add a non-solicitation clause." The draft is updated in place and a new preview is shown. 4. Send: "Send it to Dana Lee at dana@northwind.com for Northwind, and to me for Real Magic. Dana signs first." The assistant confirms the names and emails, then sends. 5. Signers sign from the email link with no account. When everyone has signed, the document is sealed and everyone gets a copy. The draft is a starting point to review, not legal advice. ### "Send this PDF for signature" 1. Attach the PDF in the chat (the paperclip, or drag it onto the message box) and say who signs: "Dana at dana@acme.com is the client; I sign for the firm." You can also just attach it and let Invisible work out the parties from the document. 2. Invisible finds every place on the document to sign or fill in (drawn lines, typed underscores, table cells, checkboxes, form fields, scanned pages too) and decides which are fields, what kind, and for whom. It completes the whole document for the signers, forms included, skipping anything already filled in and options already chosen. 3. You get a preview PDF with every field numbered and colored by signer. Correct by number: "Remove 4. Field 2 is Dana's. Add a date for me next to my signature." 4. Send: the assistant confirms names and emails, then sends exactly what you previewed. Signing, sealing and copies work as for every Invisible Sign document. ### "An investor wants a stock certificate" 1. (Once) Set up certificates: "Certificates should be signed by me as CEO and Robert as Secretary. We're a Delaware corporation with $0.00001 par value." Optionally add a custom legend (lock-ups, transfer restrictions); otherwise the standard Securities Act legend is used. 2. Issue it: "Issue a stock certificate for PA-3" or "Issue stock certificates for all of Bull City Fund III's shares." 3. The first officer gets an email and signs, then the second. The holder never signs; when both officers have signed, the holder is emailed a link to the sealed certificate. 4. If the holder asks again later, the same request re-sends the signed certificate rather than issuing a duplicate. ### "An employee wants to exercise their options" The paperwork is employee-initiated; the cap-table change is admin-controlled and gated on payment. 1. The employee asks the assistant (or opens **invisible.app/exercise**) to see what they can exercise. Only **vested** shares are exercisable — the system shows vested minus already-exercised minus anything in a pending notice. 2. The assistant shows the shares, the exercise price, and the **total cost** (shares × strike, paid to the company **outside Invisible**), and the employee types their full legal name to affirm and submits a **Notice of Exercise**. This creates an immutable signed record and a downloadable PDF, but does **not** change their holdings yet. 3. The employee pays the company for the shares offline. 4. Once funds are received, an **admin records** the exercise ("record the exercise notice for …"). This issues the shares as common stock, logs the exercise against the grant, updates the grant status, and links it to the signed notice. 5. The employee now **owns the shares** instead of holding options; the exercised portion drops off their outstanding options. An admin can reject/cancel a pending notice (e.g. wrong paperwork), and an employee can withdraw their own pending notice before it's processed. Admins can also record an exercise directly (without an employee notice) for historical or admin-initiated exercises. ### Migrating from another platform (e.g. Pulley) An admin can bring an existing cap table over: use `bulk_create_stakeholders` and `bulk_create_option_grants` for the records (keep the old identifiers via the grant number / systemId). For grants that were **already accepted** in the prior system, use `import_grant_acceptance` (or `bulk_import_grant_acceptances`) with the original acceptance date, the recorded acceptor name, and the source (e.g. "Pulley"). These are stored as clearly **migrated** records — the original date and source are preserved and the agreement PDF says so, rather than implying a fresh in-app affirmation — and the grants are marked accepted so they won't show as pending or trigger acceptance reminders. Each grant offers a "Download these terms (PDF)" copy for review even before accepting, and after acceptance a signed grant-agreement PDF. Plan language can be downloaded on its own too — from chat ("download our 2019 plan terms as a PDF") or via MCP — so employees always have a record of what they're agreeing to. ### "I want managers to see their team's equity" 1. Set up the org hierarchy: tell the assistant who reports to whom (or bulk-import from your HR system) 2. Each manager (when they log in as a viewer or employee) automatically sees equity data for their full report tree 3. To give a RevOps or HR person department-wide access, grant them department access (e.g., "Give Maria access to all Sales department equity data") 4. Admins control all visibility settings — managers cannot escalate their own access ## Access Levels | Role | Can View | Can Edit | Can Invite | |------|----------|----------|------------| | **Owner** | Everything | Everything | Yes | | **Admin** | Everything | Everything | Yes | | **Member** | Everything | Cap table data | No | | **Viewer** | Full cap table OR own holdings only (configurable) | Nothing | No | | **Employee** | Own grants and holdings only | Nothing | No | **Signers** — People signing a document through Invisible Sign don't need an Invisible account or a role. The link in their email lets them read and sign that one document and download the signed copy, nothing else. Sending, resending, voiding and downloading signed documents is limited to owners and admins. **Employees with pending grants** — When an employee logs in, the assistant automatically checks for grants pending acceptance and walks them through the process. **Visibility modifiers** (apply to Viewers and Employees): - **Manager hierarchy** — If a viewer/employee has direct or indirect reports, they also see those reports' equity data - **Department access** — Admins can grant a user view access to all stakeholders in specific departments - These combine: a user with manager access AND department access sees the union of both sets ## Glossary - **Fully Diluted** — Total shares counting all outstanding stock, preferred (on an as-converted basis), options, and available option pool - **As-Converted** — Preferred shares converted to common stock equivalent using the conversion ratio - **Anti-Dilution Ratchet** — Adjusts a preferred series' conversion ratio when a subsequent round is priced lower (down round) - **409A Valuation** — Independent appraisal of common stock fair market value, used to set option exercise prices - **Vesting** — Schedule by which equity is earned over time (typically 4 years with a 1-year cliff) - **Cliff** — Initial period before any equity vests (typically 12 months) - **ISO** — Incentive Stock Option, tax-advantaged for employees - **NQSO** — Non-Qualified Stock Option, used for contractors and advisors - **RSU** — Restricted Stock Unit, a promise to issue shares upon vesting - **RSA** — Restricted Stock Award, actual shares issued at grant that vest over time (often with an 83(b) election). Unlike an RSU, the shares exist and are owned from day one, subject to forfeiture if vesting conditions aren't met. Because RSA shares are actual restricted **Common**, Invisible models them so they count once: an RSA grant consumes the plan pool but does not create option-pool shares, and its shares are counted as issued Common (for voting, ownership %, and 409A), never a second time in fully diluted. When creating an RSA whose shares don't exist yet, set `issueShares=true` to issue the restricted Common; when the Common already exists (e.g. a migrated cap table), leave it off so nothing is double-issued. - **Liquidation Preference** — Priority claim on proceeds in an exit event (typically 1x) - **Participating Preferred** — Gets both liquidation preference AND pro-rata share of remaining proceeds - **Non-Participating Preferred** — Gets the greater of liquidation preference OR as-converted share - **Seniority** — Order in which preferred series get paid in a liquidation event - **Option Pool** — Shares reserved for future equity grants under an incentive plan - **Original Issue Price (OIP)** — Price per share when a preferred series was issued ## Connect Your AI Assistant (MCP Integration) Invisible Cap Table works with any AI assistant that supports the Model Context Protocol (MCP). This means you can query and manage your cap table directly from Claude, or any other MCP-compatible AI tool. ### Claude (claude.ai) **For organization admins — add for your whole team:** 1. Go to your Claude organization settings 2. Navigate to **Connectors** → **Add Custom Connector** 3. Enter the URL: `https://mcp.invisible.app/mcp` 4. Leave OAuth Client ID and Client Secret blank — they're not needed 5. Save the connector Everyone in your Claude org can now enable the Invisible Cap Table connector. When they first use it, they'll be asked to sign in with their invisible.app account. **For individual users — Claude Desktop or Claude Code:** Add this to your MCP configuration: ```json { "mcpServers": { "invisible-cap": { "url": "https://mcp.invisible.app/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY", "X-Organization-Id": "YOUR_ORG_ID" } } } } ``` To get your API key and org ID, ask the assistant at invisible.app: *"Generate an API key for MCP access"* ### Choosing an organization If you belong to more than one organization — for example, an advisor or a founder with two companies — an MCP connection needs to know which one you mean, because the OAuth connection itself carries only your identity, not an organization. - Ask *"what organizations do I have?"* to list them (`get_account_data` (view `my_organizations`)). - Ask *"set [company] as my active organization"* to choose one (`set_active_organization`). That choice is remembered for future sessions. - In the chat at invisible.app, switching takes effect immediately: everything after the switch, even later in the same message, acts on the new organization, and the chat starts in your chosen organization next time. Creating a new organization does not switch to it; ask to switch to it before adding data. Your first organization is chosen for you automatically, so this only comes up once you belong to several. Until you pick one, the assistant will ask you to choose rather than guess. (When connecting with an API key instead of OAuth, the organization is fixed by the key, so this step does not apply.) ### Other MCP Clients Any MCP client that supports the Streamable HTTP transport can connect to: - **URL:** `https://mcp.invisible.app/mcp` - **Auth:** Bearer token (API key) or OAuth 2.0 (authorization code + PKCE) ### What can you do via MCP? Reading uses six tools, each with a `view` (for example `get_grant_data` with view `pending_grants`); every change has its own clearly named tool, so your assistant's approval prompt tells you exactly what it's about to do. Reads don't need approval in clients that honor read-only hints, such as ChatGPT. Everything you can do in the chat, you can do via MCP: - Query the full cap table, stakeholder details, option grants - Record transactions, create grants, exercise options - Cancel whole or partial grants (returning shares to the pool), and record historical exercises without issuing new shares (for migrations) - Report a bug, request support, and check your notifications (e.g. replies to your requests) - Record terminations with post-termination exercise windows, and list upcoming or lapsed exercise deadlines - Look up vesting schedules, security class terms, equity plans - Manage stakeholders and investor groups - Invite a stakeholder to one or more entities, and link or unlink additional entities to an existing account (a VC with multiple funds) - Accept or decline grants (with typed-name affirmation) and check pending grants - Exercise vested options: see exercisable shares, submit/withdraw a signed Notice of Exercise, download the PDF; admins list, record (into the cap table), or cancel notices - Set plan and grant terms; download a grant agreement PDF - Send grant agreements for e-signature (countersigned by the option signatory), track who has signed, resend or void requests, and download the sealed, signed PDF - Issue stock certificates for common or preferred holdings, signed by two officers and sent to the holder - List your organizations and choose which one to work in - Generate spreadsheets (XLSX/CSV) and PDFs from data or analysis - Manage beneficial ownership (entity look-through) - Merge duplicate stakeholders - Send pending grant notification emails - Manage Cognito users (admin) The AI tools respect the same role-based permissions as the chat interface. Bulk tools (`bulk_create_stakeholders`, `bulk_record_transactions`, `bulk_create_option_grants`, `bulk_set_managers`) accept their collection either as a bare JSON array or as an object wrapping it, so importing an org chart or a stakeholder list from an HR system works whichever shape the client produces. Each row reports its own result, so one bad row does not stop the rest, and a tool that fails explains why rather than returning a bare failure. ## 409A Valuations Invisible Cap Table includes a built-in 409A valuation engine. A 409A valuation determines the fair market value (FMV) of your company's common stock, which is required to set exercise prices for stock option grants. Without a defensible 409A, your employees risk adverse tax consequences under IRC Section 409A. ### Two tiers **AI-Only (self-serve):** Start a valuation, provide your company financials and comparable companies, and the system runs a full multi-method analysis (DCF, Market Comps, OPM Backsolve) with DLOM calculation. You get a complete valuation report in minutes. Best for early-stage companies between formal appraisals. **AI + Reviewer:** For audit-ready valuations, assign a reviewer to the report. The reviewer (typically an outside appraiser, CFO, or board member) examines the inputs and methodology, leaves notes, and formally approves or requests changes. The report can then be finalized and locked, creating an immutable record with a defensible audit trail. ### How it works 1. **Start a valuation** -- Tell the assistant you need a 409A, or call `POST /api/409a/start` with your valuation date and company details. 2. **Provide inputs** -- Company financials (revenue, EBITDA, cash, projections), comparable public companies with EV/Revenue multiples, and DCF assumptions (discount rate, terminal growth rate). 3. **Compute** -- The system runs three valuation methods and weights them to arrive at enterprise value, then allocates to common equity and applies a DLOM (Discount for Lack of Marketability). 4. **Review (optional)** -- Assign a reviewer who can approve or request changes. 5. **Finalize** -- Lock the report. The resulting FMV is available for option pricing. ### Valuation methods - **Discounted Cash Flow (DCF)** -- Projects future cash flows and discounts them to present value using a risk-adjusted discount rate. - **Market Comparables** -- Uses EV/Revenue and EV/EBITDA multiples from comparable public companies to estimate enterprise value. - **OPM Backsolve** -- Uses the Option Pricing Method to backsolve from the most recent preferred stock price to implied common stock value, accounting for the preferred stock's economic rights (liquidation preferences, participation). - **DLOM** -- Applies a Discount for Lack of Marketability to reflect that private company shares cannot be freely traded. --- ## SBC Expensing (Stock-Based Compensation) For companies that need to track stock-based compensation expense -- whether for GAAP financial statements, investor reporting, or tax compliance -- Invisible computes it automatically. ### Black-Scholes valuation Each option grant's fair value is calculated using the Black-Scholes option pricing model with these inputs: - **Fair market value** -- From your most recent 409A valuation - **Exercise price** -- From the grant terms - **Expected term** -- Typically 5-7 years for employee options (simplified method) - **Risk-free rate** -- US Treasury yield matching the expected term - **Volatility** -- Based on comparable public companies - **Dividend yield** -- Typically zero for private companies ### Expense scheduling SBC expense is recognized over each grant's vesting period on a straight-line basis. The system generates monthly expense schedules that you can filter by: - **Date range** -- Any period you need (monthly close, quarterly reporting, annual audit) - **Department** -- Engineering, Sales, Product, Executive, etc. - **Individual grant** -- Drill into a specific grant's amortization ### ASC 718 disclosure At fiscal year-end, pull a complete ASC 718 disclosure package including: - Total SBC expense for the period - Grant activity rollforward (beginning balance, granted, exercised, forfeited, ending balance) - Weighted-average exercise prices and grant-date fair values - Weighted-average Black-Scholes assumptions used - Unamortized expense and remaining vesting period This gives your auditors everything they need for the stock-based compensation footnote. --- ## Reviewer Role and Workflow The reviewer workflow adds a layer of human oversight to AI-generated 409A valuations. This is important for audit defensibility and governance. ### Who can be a reviewer? Any organization member can be assigned as a reviewer. In practice, this is typically: - An outside valuation firm or independent appraiser - The company CFO or VP Finance - A board member or audit committee member ### Bringing in an outside reviewer You don't need a curated network — an admin can invite any third party directly: "Invite jane@appraiser.com as a reviewer." That sends them an invitation to join your organization with the reviewer role (read access to the cap table and valuations, plus the ability to sign off 409A reports — no edit access). Once they accept and appear in your members list, assign them to the report. On assignment they receive an email letting them know a 409A review is waiting, and when they sign in the assistant orients them to the reports awaiting their review. ### Workflow 1. **Admin creates and computes a valuation** -- The AI runs the full multi-method analysis. 2. **Admin invites the reviewer** (if they're not already a member) as a reviewer, and assigns them to the report -- the reviewer is emailed a notification. 3. **Reviewer examines the report** -- Reviews inputs, comparable companies, methodology weights, DCF assumptions, and the resulting FMV. 4. **Reviewer submits their decision** -- Approves the report (with optional notes) or requests changes (with notes explaining what needs to change). 5. **If changes requested** -- The admin updates inputs and re-computes. The reviewer reviews again. 6. **Admin finalizes** -- Once approved, the admin locks the report as final. No further edits are possible. ### Permissions | Action | Who can do it | |--------|--------------| | Start a valuation | Owner, Admin | | Update inputs | Owner, Admin | | Compute valuation | Owner, Admin | | Assign reviewer | Owner, Admin | | Submit review | Assigned reviewer only | | Finalize report | Owner, Admin | | View reports | All authenticated members | --- ## Invisible Sign (E-Signature) Invisible Sign is e-signature built for an AI-first workflow: the sender never touches a field editor. You ask for a document to be signed; Invisible generates it, knows where every field goes, sends it, tracks it, seals it, and writes the result back to the cap table. The only screen is the one the signer uses. ### What can be signed - **Stock option grant agreements** (`send_for_signature` with kind `grant_agreement`). Built from the grant: company, optionee, grant number, option type, shares, exercise price, grant and vesting dates, vesting schedule, expiration, plan, plan terms, and any grant-specific terms. - **Stock certificates** (`issue_stock_certificates`), for holdings of common or preferred stock. Give a certificate number (the ledger's security id, like CS-1 or PA-3) or a stakeholder for all of their stock. Options, warrants and SAFEs don't get stock certificates. A holding that a later cancellation, repurchase or transfer acted on can't be certificated; issue one for the new holding instead. - **Documents the assistant drafts** (`draft_document`): NDAs and mutual NDAs, consulting and independent contractor agreements, offer letters, services agreements, written consents, simple releases and similar. You describe the deal; the assistant writes the full text, Invisible lays it out and places every signature, name, title and date field for each party, and you get a PDF preview. Revise by chat (the same draft is updated), list drafts with `get_signing_data` (view `drafts`), see one with `get_signing_data` (view `draft`), and send with `send_drafted_document`, naming who signs as each party by email (or "me"), in parallel or in order. A sent draft can't be changed; draft a new one. Companies get By, Name and Title lines, prefilled with the person signing when known; people get Signature and Name. Drafts are a starting point to review, not legal advice. - **Your own PDFs** (attach in chat, or `upload_document` / `POST /api/sign/uploads`): up to 4.2 MB and 50 pages. `prepare_uploaded_document` places every field (signatures, dates, initials, names and titles, checkboxes, and a form's own fill-in fields) and assigns each to a signer, named by you or read from the document; scanned PDFs work too. The preview numbers every field; fix anything with `edit_document_fields` (remove, change signer or type, add at a spot listed by `get_signing_data` (view `draft`) with `includeSpots`), then send with `send_drafted_document`. A PDF with edit restrictions (an owner password) is copied into a plain PDF so it can be signed, and you're told; one that needs a password to open is refused. ### Who signs, and in what order - **Option signatory** — the person who countersigns grant agreements for the company, following the Carta and Pulley convention. Set with `set_option_signatory` (an owner or admin, plus the title printed on the agreement). They sign first. - **Drafted and uploaded documents** — whoever the sender names for each party or signer, by email, including themself. Parallel (everyone at once) by default, or in order. Every signer of an uploaded document needs at least one signature or initials field. - **Optionee** — signs second. Their email comes from their stakeholder record (or linked account), never from what the sender types, so an agreement cannot be redirected. A stakeholder with no email on file has to have one added before sending. - Without an option signatory, the agreement goes to the optionee alone. - **Stock certificates** are signed by two different officers (the Delaware default), set once with `set_certificate_settings` along with the state of incorporation, par value per share and any custom legend. The holder is a **copy recipient**: they don't sign, and they're emailed the sealed certificate when both officers have. ### The signing experience 1. The signer opens the link in their email. No login. 2. They review and agree to the electronic records disclosure (the ESIGN consent). Which version they saw is recorded. **They only do this once per company**: the consent covers every document that company sends them through Invisible Sign, so on the next document they go straight to signing, and that request's record notes the consent on file and when it was given. The signing page shows when they agreed, with a **Withdraw consent** link; after withdrawing, they are asked again before signing anything electronically, and anything already signed stays valid. If the disclosure wording changes, everyone is asked again. 3. The document is shown with their fields highlighted. Their printed name and title are prefilled and editable; the date is filled in automatically when they sign. 4. They adopt a signature, drawn with a finger or mouse or typed, and select **Finish and sign**. They can also decline, with an optional reason, which closes the request for everyone and notifies the sender. 5. Once everyone has signed, they can download the sealed copy from the same page or from the completion email. Links expire after 30 days. Resending issues fresh links and extends the expiry; the old links stop working. ### What you get when it's complete - **The signed agreement** with every signature, name, title and date stamped in place. - **A certificate of completion** appended to the document: each signer's email, role, and every step they took (sent, viewed, consented or consent on file with the date it was given, signed) with time, IP address and browser, plus the original document's SHA-256 hash. - **A digital seal** over the whole file, issued to Real Magic through SSL.com, so Adobe Acrobat shows the document as signed and unaltered. Any change after sealing breaks the seal. - **Tamper-proof storage**: the sealed file is stored write-once for seven years. - **The cap table updated** (grant agreements): the grant is marked accepted, with an acceptance record linked to the signature request. ### Reminders - **Signers are reminded automatically**: every 3 days, up to 3 times, until they sign. Only whoever's turn it is gets a reminder (in a sequential request, later signers wait). Each reminder is a fresh link, so earlier links stop working; reminders never extend the expiry, and when a link is close to expiring the reminder says so. Every reminder is in the evidence trail and on the certificate of completion. - **This covers every Invisible Sign document**: grant agreements, stock certificates, drafted and uploaded documents. - **Change the defaults** with `set_reminder_settings` ("remind signers every 2 days, up to 5 times", or turn them off). Defaults apply to documents sent from then on. - **One document**: `set_request_reminders` turns reminders off or changes the schedule for a request already out ("stop reminding Dana about the NDA"). `get_signing_data` (view `signature_request`) shows each request's schedule and how many reminders each signer has had. - **Grant acceptances** (the cap table's typed-name acceptance): off by default; `set_reminder_settings` with `grantAcceptanceReminders` turns on weekly reminders, up to 3, for employees whose grants are waiting. See "Employees need to accept their grants". ### Status values `sent` (out for signature), `in_progress` (at least one person signed), `completed`, `declined`, `voided`, `expired`. Use `get_signing_data` (view `signature_request`) to see each recipient's status and the full evidence trail. ### Current limits - Signers are verified by their email link only; SMS or access-code verification is planned. - Reminders go out about hourly once due, so a reminder can arrive up to an hour after its day comes. - The typed-name grant acceptance still works and is unchanged. ## API Documentation Full API reference for building integrations: - **Markdown (AI-friendly):** https://api.invisible.app/docs/api - **OpenAPI 3.0 spec:** https://api.invisible.app/docs/openapi.yaml These docs are optimized for use with AI coding assistants. Feed them to Claude, Cursor, or Copilot to generate integration code. ## Pricing Invisible Cap Table is currently in early access. Contact us for pricing information. ## Invitations Every invitation works the same way, whether it grants a role in the organization or links someone to their own equity records: 1. An owner or admin invites someone by email through chat or MCP. 2. Invisible emails them a link to `invisible.app/invite/CODE`. 3. Opening the link shows who invited them, the organization, and the role. 4. They choose a password, confirm the 6-digit code emailed to them, and are in (an existing account just signs in). Invitations expire after 7 days by default and can only be accepted once. Nobody is ever added to an organization without accepting. Inviting someone who already has an Invisible account creates an invitation for them to accept rather than adding them silently, so they always find out they were given access. Pending invitations are addressed to an email, not an account, so they are waiting whenever that person signs up. Ask the assistant "what have I been invited to?" at any time to see them. ## Getting Started Just start chatting. If you're new, the assistant will guide you through setting up your organization and loading your cap table data. If someone invited you, open the link in your invitation email — that is the whole process. You'll see who invited you and what you're joining, choose a password, and land inside. If you sign up directly at invisible.app rather than through a link, the assistant checks for invitations addressed to your email as soon as you're in. If any are waiting it lists them and asks which you'd like to accept, so you can join some and not others. If there are none, it offers to create an organization for your cap table. # Invisible Cap Table API Reference Base URL: `https://api.invisible.app` JSON naming: all request/response fields use **camelCase**. Enum values use **camelCase** (e.g., `"common"`, `"convertibleNote"`). --- ## 1. Authentication All endpoints except `/health`, `/api/chat/public`, and OAuth metadata require authentication. ### Method A: API Key Get a key by asking the chat assistant at [invisible.app](https://invisible.app). ``` Authorization: Bearer icap_YOUR_KEY ``` The API key is scoped to a single organization. No additional headers needed. ### Method B: JWT (Cognito) ``` Authorization: Bearer X-Organization-Id: ``` `POST /api/chat` and `POST /api/chat/public` take an optional `X-Invisible-Product` header, `cap` (default) or `sign`, choosing which product's assistant answers; sign.invisible.app sends `sign`. The Sign assistant offers only Invisible Sign, account and support tools. Using a product turns it on for the organization. `X-Organization-Id` is required for all endpoints except `/api/me`, `/api/organizations`, `/api/invites/*`, `/api/org-invites/redeem`, `/api/chat`, and `/api/conversations`. Endpoints under `/api/public/*` need no authentication at all. ### Role-Based Access | Role | Read cap table | Write cap table | Manage members | |------|---------------|-----------------|----------------| | `owner` | Full | Yes | Yes | | `admin` | Full | Yes | Yes | | `member` | Full | Yes | No | | `viewer` | Full (if `canViewFullCapTable=true`) or own holdings only | No | No | | `employee` | Own holdings only | No | No | Write endpoints (`POST`, `PUT`, `DELETE` on cap table data) return `403` for `viewer` and `employee` roles. **Visibility modifiers** for restricted viewers (viewer without full cap table, employee): - **Manager hierarchy**: If the user's linked stakeholder has reports (via `manager_id`), they see the full subtree of reports' equity data. - **Department access**: Admins can grant users view access to all equity data in specific departments via `department_access`. - The user sees the union of: own holdings + report tree + department access. --- ## 2. Cap Table (Read) ### GET /api/cap-table Full cap table with all stakeholders, summary, security classes, and investor groups. **Auth:** Required. Restricted viewers see only their own holdings. ```bash curl -s https://api.invisible.app/api/cap-table \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "stakeholders": [ { "id": "a1b2c3d4-0001-4000-8000-000000000001", "name": "Sarah Chen", "type": "individual", "commonShares": 4000000, "preferredShares": 0, "preferredSeriesDetail": [], "optionShares": 0, "fullyDilutedShares": 4000000, "ownershipPct": 40.0 }, { "id": "a1b2c3d4-0001-4000-8000-000000000002", "name": "Sequoia Capital", "type": "entity", "commonShares": 0, "preferredShares": 2000000, "preferredSeriesDetail": [ { "series": "Series A Preferred", "shares": 2000000, "asConvertedShares": 2000000 } ], "optionShares": 0, "fullyDilutedShares": 2000000, "ownershipPct": 20.0 } ], "summary": { "totalCommon": 6000000, "totalPreferred": 2000000, "totalOptionsOutstanding": 500000, "totalFullyDiluted": 10000000, "optionPoolAuthorized": 1000000, "optionPoolAvailable": 500000 }, "securityClasses": [ { "id": "b2c3d4e5-0001-4000-8000-000000000001", "name": "Common Stock", "type": "common", "authorizedShares": 8000000, "outstandingShares": 6000000, "originalIssuePrice": null, "liquidationPreference": null, "conversionRatio": 1.0, "isParticipating": false, "participationCap": null, "seniorityRank": null }, { "id": "b2c3d4e5-0001-4000-8000-000000000002", "name": "Series A Preferred", "type": "preferred", "authorizedShares": 3000000, "outstandingShares": 2000000, "originalIssuePrice": 1.50, "liquidationPreference": 1.0, "conversionRatio": 1.0, "isParticipating": false, "participationCap": null, "seniorityRank": 1 } ], "investorGroups": [ { "id": "c3d4e5f6-0001-4000-8000-000000000001", "name": "Series A Investors", "totalShares": 2000000, "ownershipPct": 20.0, "memberIds": ["a1b2c3d4-0001-4000-8000-000000000002"] } ] } ``` --- ### GET /api/stakeholders List all stakeholders. Supports search. **Auth:** Required. Restricted viewers see only themselves. **Query params:** | Param | Type | Description | |-------|------|-------------| | `search` | string | Filter by name (partial match) | ```bash curl -s "https://api.invisible.app/api/stakeholders?search=chen" \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "stakeholders": [ { "id": "a1b2c3d4-0001-4000-8000-000000000001", "name": "Sarah Chen", "type": "individual", "email": "sarah@acme.com", "jobTitle": "CEO", "department": "Executive", "managerId": null, "managerName": null, "isActive": true } ] } ``` --- ### GET /api/stakeholders/{id} Detailed view of a single stakeholder: direct holdings, transactions, option grants, and beneficial ownership in both directions. Equity is often held indirectly, through an LLC or a fund rather than in someone's own name. `beneficialInterests` lists entities this stakeholder holds an interest in; `beneficialOwners` is populated when this stakeholder is itself an entity and lists who holds interests in it. A person's real position is their direct holdings plus their beneficial interests, so reporting `holdings` alone can understate what they own. **Auth:** Required. Restricted viewers can only access their own linked stakeholder. ```bash curl -s https://api.invisible.app/api/stakeholders/a1b2c3d4-0001-4000-8000-000000000001 \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "stakeholder": { "id": "a1b2c3d4-0001-4000-8000-000000000001", "organizationId": "d4e5f6a7-0001-4000-8000-000000000001", "name": "Sarah Chen", "type": "individual", "email": "sarah@acme.com", "jobTitle": "CEO", "department": "Executive", "employeeId": "EMP-001", "userId": null, "isActive": true, "createdAt": "2024-01-15T00:00:00Z", "updatedAt": "2024-01-15T00:00:00Z" }, "holdings": [ { "securityClass": "Common Stock", "shares": 4000000, "pctOwnership": 40.0 } ], "transactions": [ { "id": "e5f6a7b8-0001-4000-8000-000000000001", "securityId": "CS-001", "type": "issuance", "shares": 4000000, "pricePerShare": 0.001, "date": "2024-01-15T00:00:00Z", "notes": "Founder shares" } ], "optionGrants": [], "beneficialInterests": [ { "id": "b7c8d9e0-0001-4000-8000-000000000001", "entityStakeholderId": "a1b2c3d4-0009-4000-8000-000000000009", "entityName": "Acme Holdings LLC", "ownerStakeholderId": "a1b2c3d4-0001-4000-8000-000000000001", "ownerName": "Sarah Chen", "ownershipPct": 0.9107, "sharesEquivalent": 54642, "notes": "From LLC cap table (conversion to Inc 01.23.23)" } ], "beneficialOwners": [] } ``` --- ### GET /api/security-classes List all security classes for the organization. **Auth:** Required. ```bash curl -s https://api.invisible.app/api/security-classes \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "classes": [ { "id": "b2c3d4e5-0001-4000-8000-000000000001", "name": "Common Stock", "type": "common", "authorizedShares": 8000000, "outstandingShares": 6000000, "originalIssuePrice": null, "liquidationPreference": null, "conversionRatio": 1.0, "isParticipating": false, "participationCap": null, "seniorityRank": null } ] } ``` --- ### GET /api/equity-plans List all equity incentive plans. **Auth:** Required. ```bash curl -s https://api.invisible.app/api/equity-plans \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "plans": [ { "id": "f6a7b8c9-0001-4000-8000-000000000001", "organizationId": "d4e5f6a7-0001-4000-8000-000000000001", "name": "2024 Equity Incentive Plan", "year": 2024, "authorizedShares": 1000000, "createdAt": "2024-01-15T00:00:00Z", "sharesOutstanding": 410000, "sharesExercised": 25000, "sharesAvailable": 590000, "postTerminationExerciseDays": 90 } ] } ``` `sharesOutstanding` is net of cancellations (a partially cancelled grant returns only the cancelled portion); exercised options stay counted as consumed. `sharesAvailable` = authorized − outstanding. --- ## 3. Option Grants ### GET /api/option-grants List option grants with optional filters. **Auth:** Required. Restricted viewers see only their own grants. **Query params:** | Param | Type | Description | |-------|------|-------------| | `stakeholderId` | uuid | Filter by stakeholder | | `department` | string | Filter by department | | `status` | string | Comma-separated. Values: `proposed`, `pendingApproval`, `approved`, `active`, `fullyVested`, `exercised`, `partiallyExercised`, `cancelled`, `expired` | | `planName` | string | Filter by equity plan name | | `asOfDate` | string | Calculate vesting as of this date (`YYYY-MM-DD`) | ```bash curl -s "https://api.invisible.app/api/option-grants?status=active,fullyVested&department=Engineering" \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "grants": [ { "id": "11111111-0001-4000-8000-000000000001", "employeeName": "James Park", "jobTitle": "Senior Engineer", "department": "Engineering", "grantNumber": "OPT-2024-001", "optionType": "iso", "grantDate": "2024-03-01T00:00:00Z", "vestingStartDate": "2024-03-01T00:00:00Z", "exercisePrice": 0.50, "sharesGranted": 50000, "sharesVested": 12500, "sharesUnvested": 37500, "sharesExercised": 0, "sharesCancelled": 0, "sharesExercisable": 12500, "expirationDate": "2034-03-01T00:00:00Z", "status": "active", "category": "newHire", "notes": null, "planName": "2024 Equity Incentive Plan", "vestingScheduleId": "a1b2c3d4-0001-4000-8000-000000000001", "vestingScheduleName": "4-year monthly, 1-year cliff", "vestingFrequency": "monthly", "vestingTotalMonths": 48, "vestingCliffMonths": 12, "terminatedAt": null, "exerciseDeadline": null, "lapsed": false } ] } ``` `exerciseDeadline` is the earlier of the option's expiration and, for a terminated holder, the post-termination deadline (explicit date, or termination date + the grant's window override or the plan's `postTerminationExerciseDays`). After it passes, `lapsed` is true, `sharesExercisable` is 0, and the unexercised options no longer count as outstanding or against the pool. Vesting stops at `terminatedAt`. Cancelled shares (e.g. the unvested remainder forfeited at termination) come out of the unvested portion: vested is capped at granted minus cancelled, and unvested is what remains of that. Status filters and all enum inputs accept `snake_case` (`partially_exercised`), `camelCase`, or `PascalCase`; tool output uses `snake_case`. Vested shares honor the schedule's frequency: `monthly`, `quarterly`, and `annually` schedules vest in whole tranches (elapsed months are rounded down to the last completed tranche), and the cliff tranche vests in full once reached. --- ### POST /api/option-grants Create an option grant. Uses the **preview/confirm pattern**: first call with `confirm: false` to see impact, then call with `confirm: true` to commit. **Auth:** Required. `owner`, `admin`, or `member` role. **Enum values:** - `optionType`: `iso`, `nqso`, `rsu`, `rsa` (rsa = restricted stock award: actual shares issued at grant, distinct from rsu) - `status`: `proposed`, `pendingApproval`, `approved`, `active`, `fullyVested`, `exercised`, `partiallyExercised`, `cancelled`, `expired` - `category`: `newHire`, `promotion`, `retention`, `performance`, `refresh`, `advisory`, `other` #### Step 1: Preview (confirm=false) ```bash curl -s -X POST https://api.invisible.app/api/option-grants \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "equityPlanId": "f6a7b8c9-0001-4000-8000-000000000001", "optionType": "iso", "sharesGranted": 50000, "exercisePrice": 0.50, "grantDate": "2024-03-01", "vestingStartDate": "2024-03-01", "expirationDate": "2034-03-01", "vestingScheduleId": "22222222-0001-4000-8000-000000000001", "status": "active", "category": "newHire", "grantNumber": "OPT-2024-001", "notes": "New hire grant for Senior Engineer", "confirm": false }' ``` **Preview response:** ```json { "preview": { "grantDetail": { "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "equityPlanId": "f6a7b8c9-0001-4000-8000-000000000001", "optionType": "iso", "sharesGranted": 50000, "exercisePrice": 0.50, "grantDate": "2024-03-01", "vestingStartDate": "2024-03-01", "expirationDate": "2034-03-01", "vestingScheduleId": "22222222-0001-4000-8000-000000000001", "status": "active", "category": "newHire", "grantNumber": "OPT-2024-001", "notes": "New hire grant for Senior Engineer", "confirm": false }, "poolImpact": { "availableBefore": 500000, "availableAfter": 450000, "pctPoolUsed": 55.0 }, "validationWarnings": [] } } ``` #### Step 2: Commit (confirm=true) Same request body with `"confirm": true`. ```bash curl -s -X POST https://api.invisible.app/api/option-grants \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "equityPlanId": "f6a7b8c9-0001-4000-8000-000000000001", "optionType": "iso", "sharesGranted": 50000, "exercisePrice": 0.50, "grantDate": "2024-03-01", "vestingStartDate": "2024-03-01", "expirationDate": "2034-03-01", "vestingScheduleId": "22222222-0001-4000-8000-000000000001", "status": "active", "category": "newHire", "grantNumber": "OPT-2024-001", "confirm": true }' ``` **Commit response:** ```json { "grantId": "11111111-0001-4000-8000-000000000001", "preview": { "grantDetail": { "..." : "same as above" }, "poolImpact": { "availableBefore": 500000, "availableAfter": 450000, "pctPoolUsed": 55.0 }, "validationWarnings": [] } } ``` --- ### PUT /api/option-grants Update an existing option grant. Only provided fields are modified. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X PUT https://api.invisible.app/api/option-grants \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "grantId": "11111111-0001-4000-8000-000000000001", "sharesGranted": 60000, "exercisePrice": 0.75, "category": "promotion", "notes": "Adjusted after promotion to Staff Engineer", "status": "active" }' ``` **Response:** ```json { "updated": true } ``` Returns `404` if grant not found. --- ### DELETE /api/option-grants/{id} Delete an option grant. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X DELETE https://api.invisible.app/api/option-grants/11111111-0001-4000-8000-000000000001 \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "deleted": true } ``` Returns `404` if grant not found. --- ### POST /api/option-grants/exercise Exercise vested options. Uses **preview/confirm pattern**. **Auth:** Required. `owner`, `admin`, or `member` role. #### Preview ```bash curl -s -X POST https://api.invisible.app/api/option-grants/exercise \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "grantId": "11111111-0001-4000-8000-000000000001", "sharesToExercise": 10000, "exerciseDate": "2025-06-15", "notes": "Partial exercise", "confirm": false }' ``` **Preview response:** ```json { "preview": { "grantId": "11111111-0001-4000-8000-000000000001", "sharesGranted": 50000, "sharesVested": 12500, "sharesPreviouslyExercised": 0, "sharesExercisable": 12500, "sharesToExercise": 10000, "sharesRemainingAfter": 2500, "newStatus": "partiallyExercised", "validationWarnings": [] } } ``` #### Commit Same body with `"confirm": true`. **Commit response:** ```json { "exerciseId": "33333333-0001-4000-8000-000000000001", "preview": { "...": "same as above" } } ``` --- ### POST /api/grant-status Batch-update the status of one or more grants. Uses **preview/confirm pattern**. **Auth:** Required. `owner`, `admin`, or `member` role. #### Preview ```bash curl -s -X POST https://api.invisible.app/api/grant-status \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "grantIds": [ "11111111-0001-4000-8000-000000000001", "11111111-0001-4000-8000-000000000002" ], "newStatus": "approved", "notes": "Board approved Q1 grants", "confirm": false }' ``` **Preview response:** ```json { "grantsAffected": 2, "confirmRequired": true } ``` #### Commit ```bash curl -s -X POST https://api.invisible.app/api/grant-status \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "grantIds": [ "11111111-0001-4000-8000-000000000001", "11111111-0001-4000-8000-000000000002" ], "newStatus": "approved", "notes": "Board approved Q1 grants", "confirm": true }' ``` **Commit response:** ```json { "success": true, "grantsAffected": 2 } ``` --- ### GET /api/vesting-schedules List all vesting schedule templates. **Auth:** Required. ```bash curl -s https://api.invisible.app/api/vesting-schedules \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "schedules": [ { "id": "22222222-0001-4000-8000-000000000001", "name": "Standard 4-year with 1-year cliff", "totalMonths": 48, "cliffMonths": 12, "frequency": "monthly", "createdAt": "2024-01-15T00:00:00Z" } ] } ``` --- ### POST /api/vesting-schedules Create a new vesting schedule template. **Auth:** Required. `owner`, `admin`, or `member` role. **Enum values for `frequency`:** `monthly`, `quarterly`, `annually` ```bash curl -s -X POST https://api.invisible.app/api/vesting-schedules \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "3-year quarterly with 6-month cliff", "totalMonths": 36, "cliffMonths": 6, "frequency": "quarterly" }' ``` **Response:** ```json { "vestingScheduleId": "22222222-0001-4000-8000-000000000002" } ``` --- ## Grant Acceptance ### POST /api/grants/{grantId}/accept Accepts a stock option grant. The grant must belong to a stakeholder linked to the current user. Records acceptance with timestamp. **Auth:** Required. Any authenticated user (validates grant ownership). ```bash curl -s -X POST https://api.invisible.app/api/grants/11111111-0001-4000-8000-000000000001/accept \ -H "Authorization: Bearer " \ -H "X-Organization-Id: d4e5f6a7-0001-4000-8000-000000000001" ``` **Response:** ```json { "accepted": true, "grantNumber": "511", "sharesGranted": 4000, "exercisePrice": 2.80, "message": "Grant #511 accepted..." } ``` --- ### GET /api/grants/pending Returns active grants pending acceptance for the current user. **Auth:** Required. Any authenticated user. ```bash curl -s https://api.invisible.app/api/grants/pending \ -H "Authorization: Bearer " \ -H "X-Organization-Id: d4e5f6a7-0001-4000-8000-000000000001" ``` **Response:** ```json { "pendingGrants": [ { "grantId": "11111111-0001-4000-8000-000000000001", "grantNumber": "511", "sharesGranted": 4000, "exercisePrice": 2.80, "grantDate": "2026-03-02", "expirationDate": "2036-03-02", "planName": "2019 Equity Incentive Plan", "totalMonths": 48, "cliffMonths": 12 } ] } ``` --- ### GET /api/grants/pending/all Lists all employees with pending grants (admin summary view). **Auth:** Required. `owner` or `admin` role. ```bash curl -s https://api.invisible.app/api/grants/pending/all \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "employees": [ { "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "stakeholderName": "Grant Kitching", "email": "grant@example.com", "grantCount": 2, "totalShares": 6500 } ] } ``` --- ### POST /api/grants/pending/notify Sends email notifications to employees with pending grants. Optionally target a single stakeholder. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/grants/pending/notify \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003" }' ``` Omit `stakeholderId` to notify all employees with pending grants. **Response:** ```json { "sent": 15, "skipped": 3, "total": 18 } ``` --- ## Option Exercise Employees exercise vested options via a signed **Notice of Exercise**; an admin records it into the cap table once payment (shares × strike) is received offline. Submitting a notice does not change holdings; recording it issues the shares as common stock. Only vested shares are exercisable (no early exercise). ### GET /api/grants/exercisable Returns the current user's grants with exercisable share counts (vested − already-exercised − pending-notice shares). **Auth:** Required. Any authenticated user (their own grants only). ```bash curl -s https://api.invisible.app/api/grants/exercisable \ -H "Authorization: Bearer " ``` **Response:** ```json { "exercisableGrants": [ { "grantId": "11111111-0001-4000-8000-000000000001", "grantNumber": "511", "optionType": "Iso", "planName": "2019 Equity Incentive Plan", "sharesGranted": 4000, "exercisePrice": 2.80, "grantDate": "2026-03-02", "expirationDate": "2036-03-02", "sharesVested": 2000, "sharesExercised": 0, "sharesPendingNotice": 0, "sharesExercisable": 2000, "status": "active" } ] } ``` --- ### POST /api/grants/{grantId}/exercise-notice Submits a Notice of Exercise for vested shares. Requires the employee's typed full legal name. Does not change the cap table. **Auth:** Required. The grant must belong to the current user. ```bash curl -s -X POST https://api.invisible.app/api/grants/11111111-0001-4000-8000-000000000001/exercise-notice \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"sharesToExercise": 1000, "affirmationName": "Jane Q. Employee"}' ``` **Request body:** `{ "sharesToExercise": number, "affirmationName": string }` **Response (200):** ```json { "ok": true, "noticeId": "aaaa1111-0001-4000-8000-000000000001", "status": "pending", "grantNumber": "511", "sharesToExercise": 1000, "exercisePrice": 2.80, "totalCost": 2800.00, "message": "Notice of Exercise submitted for 1,000 shares at $2.80 (total $2,800.00). Your administrator will record it once payment is received. A signed PDF is available on request." } ``` Returns **400** with `ok: false` if the shares exceed what's exercisable or the affirmation name is missing. --- ### GET /api/exercise-notices Lists the current user's own exercise notices. **Auth:** Required. ```bash curl -s https://api.invisible.app/api/exercise-notices \ -H "Authorization: Bearer " ``` **Response:** `{ "notices": [ { "noticeId", "grantId", "grantNumber", "sharesToExercise", "exercisePrice", "totalCost", "status", "affirmationName", "submittedAt", "processedAt" } ] }` — `status` is `pending`, `recorded`, `cancelled`, or `withdrawn`. --- ### POST /api/exercise-notices/{noticeId}/withdraw Withdraws the current user's own pending notice. **Auth:** Required. Only the notice's owner, only while pending. ```bash curl -s -X POST https://api.invisible.app/api/exercise-notices/aaaa1111-0001-4000-8000-000000000001/withdraw \ -H "Authorization: Bearer " ``` **Response:** `{ "ok": true, "status": "withdrawn", "message": "Your Notice of Exercise has been withdrawn." }` --- ### GET /api/exercise-notices/{noticeId}/pdf Returns the signed Notice of Exercise as a PDF. The owner can fetch their own; admins can fetch any in the org. **Auth:** Required. ```bash curl -s https://api.invisible.app/api/exercise-notices/aaaa1111-0001-4000-8000-000000000001/pdf \ -H "Authorization: Bearer " -o notice.pdf ``` **Response:** `application/pdf` bytes. **Recording, cancelling, and admin listing** are available through the chat/MCP tools (`get_grant_data` (view `exercise_notices`), `record_exercise_notice`, `cancel_exercise_notice`) rather than dedicated REST endpoints — recording an exercise mutates the cap table and is an admin action. --- ## 4. Transactions ### GET /api/transactions Transaction history with optional filters. **Auth:** Required. **Query params:** | Param | Type | Description | |-------|------|-------------| | `stakeholderId` | uuid | Filter by stakeholder | | `securityClassId` | uuid | Filter by security class | | `type` | string | One of: `issuance`, `grant`, `exercise`, `transfer`, `cancellation`, `repurchase`, `conversion` | | `startDate` | string | Start date (`YYYY-MM-DD`) | | `endDate` | string | End date (`YYYY-MM-DD`) | ```bash curl -s "https://api.invisible.app/api/transactions?type=issuance&startDate=2024-01-01&endDate=2024-12-31" \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "transactions": [ { "id": "e5f6a7b8-0001-4000-8000-000000000001", "securityId": "CS-001", "type": "issuance", "shares": 4000000, "pricePerShare": 0.001, "date": "2024-01-15T00:00:00Z", "notes": "Founder shares" } ] } ``` --- ### POST /api/transactions Record a transaction. Uses **preview/confirm pattern**. **Auth:** Required. `owner`, `admin`, or `member` role. **Enum values for `type`:** `issuance`, `grant`, `exercise`, `transfer`, `cancellation`, `repurchase`, `conversion` #### Preview ```bash curl -s -X POST https://api.invisible.app/api/transactions \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "securityId": "SA-001", "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000002", "securityClassId": "b2c3d4e5-0001-4000-8000-000000000002", "type": "issuance", "shares": 2000000, "pricePerShare": 1.50, "transactionDate": "2024-06-01", "notes": "Series A investment", "confirm": false }' ``` **Preview response:** ```json { "preview": { "transactionDetail": { "securityId": "SA-001", "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000002", "securityClassId": "b2c3d4e5-0001-4000-8000-000000000002", "type": "issuance", "shares": 2000000, "pricePerShare": 1.50, "transactionDate": "2024-06-01", "notes": "Series A investment", "confirm": false }, "validationWarnings": [], "impact": { "sharesBefore": 0, "sharesAfter": 2000000, "ownershipPctChange": 20.0 } } } ``` #### Commit Same body with `"confirm": true`. **Commit response:** ```json { "transactionId": "e5f6a7b8-0001-4000-8000-000000000002", "preview": { "...": "same as above" } } ``` --- ## 5. Organization Management ### GET /api/me Current user profile with all org memberships and linked stakeholders. Each membership lists the org's `products` (`cap` for Invisible Cap Table, `sign` for Invisible Sign). **Auth:** Required. No `X-Organization-Id` needed. ```bash curl -s https://api.invisible.app/api/me \ -H "Authorization: Bearer " ``` **Response:** ```json { "userId": "44444444-0001-4000-8000-000000000001", "email": "sarah@acme.com", "name": "Sarah Chen", "organizations": [ { "organizationId": "d4e5f6a7-0001-4000-8000-000000000001", "organizationName": "Acme Inc", "role": "owner", "canViewFullCapTable": true, "products": ["cap", "sign"] } ], "linkedStakeholders": [ { "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000001", "organizationId": "d4e5f6a7-0001-4000-8000-000000000001", "organizationName": "Acme Inc", "name": "Sarah Chen", "type": "individual" } ] } ``` --- ### POST /api/organizations Create a new organization. The creating user becomes `owner`. **Auth:** Required (JWT). No `X-Organization-Id` needed. ```bash curl -s -X POST https://api.invisible.app/api/organizations \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Inc", "slug": "acme", "domain": "acme.com", "products": ["cap"] }' ``` `products` is optional: `["cap"]` (Invisible Cap Table, the default), `["sign"]` (Invisible Sign), or both. **Response:** ```json { "organizationId": "d4e5f6a7-0001-4000-8000-000000000001" } ``` --- ### GET /api/organizations/{id}/members List all members of an organization. **Auth:** Required. `owner` or `admin` role. ```bash curl -s https://api.invisible.app/api/organizations/d4e5f6a7-0001-4000-8000-000000000001/members \ -H "Authorization: Bearer " \ -H "X-Organization-Id: d4e5f6a7-0001-4000-8000-000000000001" ``` **Response:** ```json { "members": [ { "id": "55555555-0001-4000-8000-000000000001", "organizationId": "d4e5f6a7-0001-4000-8000-000000000001", "userId": "44444444-0001-4000-8000-000000000001", "role": "owner", "canViewFullCapTable": true, "createdAt": "2024-01-15T00:00:00Z" } ] } ``` --- ### POST /api/organizations/{id}/members Invite a user to the organization. If the user already has an account, they are added immediately. Otherwise, an invite code is generated. **Auth:** Required. `owner` or `admin` role. **Enum values for `role`:** `owner`, `admin`, `member`, `viewer`, `employee` ```bash curl -s -X POST https://api.invisible.app/api/organizations/d4e5f6a7-0001-4000-8000-000000000001/members \ -H "Authorization: Bearer " \ -H "X-Organization-Id: d4e5f6a7-0001-4000-8000-000000000001" \ -H "Content-Type: application/json" \ -d '{ "email": "james@acme.com", "role": "member", "expiresInDays": 7 }' ``` **Response (user exists):** ```json { "added": true, "message": "User james@acme.com added to organization as member" } ``` **Response (user must sign up):** ```json { "added": false, "inviteCode": "abc123def456", "message": "Invite created for james@acme.com (user must sign up first)" } ``` --- ### PUT /api/organizations/{id}/members/{userId} Update a member's role or cap table visibility. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X PUT https://api.invisible.app/api/organizations/d4e5f6a7-0001-4000-8000-000000000001/members/44444444-0001-4000-8000-000000000002 \ -H "Authorization: Bearer " \ -H "X-Organization-Id: d4e5f6a7-0001-4000-8000-000000000001" \ -H "Content-Type: application/json" \ -d '{ "role": "admin", "canViewFullCapTable": true }' ``` **Response:** ```json { "updated": true } ``` --- ### POST /api/org-invites/redeem Redeem an organization invite code. **Auth:** Required (JWT). No `X-Organization-Id` needed. ```bash curl -s -X POST https://api.invisible.app/api/org-invites/redeem \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "inviteCode": "abc123def456" }' ``` **Response:** ```json { "redeemed": true } ``` --- ### POST /api/organizations/{id}/invite Create a stakeholder invite that links a user account to a stakeholder record (e.g., so an employee can view their own holdings). **Auth:** Required. `owner` or `admin` role. **Optional fields:** | Field | Type | Description | |-------|------|-------------| | `departments` | string[] | Department view access to grant on invite redemption | | `canViewFullCapTable` | boolean | Whether to grant full cap table visibility on invite redemption | These permissions are embedded in the invite and applied automatically when redeemed. ```bash curl -s -X POST https://api.invisible.app/api/organizations/d4e5f6a7-0001-4000-8000-000000000001/invite \ -H "Authorization: Bearer " \ -H "X-Organization-Id: d4e5f6a7-0001-4000-8000-000000000001" \ -H "Content-Type: application/json" \ -d '{ "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "email": "james@acme.com", "expiresInDays": 7, "departments": ["Engineering", "Product"], "canViewFullCapTable": false }' ``` **Response:** ```json { "inviteCode": "xyz789abc012", "expiresInDays": 7 } ``` --- ### POST /api/invites/redeem Redeem a stakeholder invite code (links the authenticated user to the stakeholder). **Auth:** Required (JWT). No `X-Organization-Id` needed. ```bash curl -s -X POST https://api.invisible.app/api/invites/redeem \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "inviteCode": "xyz789abc012" }' ``` **Response:** ```json { "redeemed": true } ``` --- ## Invitations An invitation is addressed to an email address, not to an account, so it can be created before the recipient has ever heard of Invisible. Accepting is always an explicit act: inviting someone who already has an account creates an invitation for them rather than adding them to the organization silently. ### GET /api/public/invites/{code} Describe an invite code so the invitation page can be rendered before the recipient has an account. Returns only what that page needs to show. **Auth:** None. The invite code is the bearer secret. ```bash curl -s https://api.invisible.app/api/public/invites/aB3dE5gH7jK9mN1pQ2r ``` **Response:** ```json { "code": "aB3dE5gH7jK9mN1pQ2r", "kind": "organization", "status": "valid", "organizationName": "Real Magic, Inc.", "inviterName": "Jes Lipson", "invitedEmail": "jane@sequoia.com", "role": "viewer", "stakeholderName": null, "hasAccount": false } ``` `kind` is `organization` or `stakeholder`. `status` is `valid`, `expired`, `alreadyaccepted`, or `not_found`. `hasAccount` says whether that email already has a login, which decides whether the page asks for a new password or an existing one. Note this returns **200 with `"status": "not_found"`** for an unknown code rather than a 404. The CloudFront distribution in front of `invisible.app` rewrites any 403 or 404 from any origin into the SPA's `index.html`, so a 404 here would reach the browser as HTML. ### POST /api/public/invites/{code}/confirm-signup Confirm a Cognito account that was just created through an invitation link, skipping the six-digit email verification code. Receiving the emailed invite already proves the recipient controls that inbox. The code must be valid, unredeemed, and addressed to exactly this email — without that check the endpoint would confirm any account in the pool. **Auth:** None. ```bash curl -s -X POST https://api.invisible.app/api/public/invites/aB3dE5gH7jK9mN1pQ2r/confirm-signup \ -H "Content-Type: application/json" \ -d '{ "email": "jane@sequoia.com" }' ``` **Response:** ```json { "confirmed": true } ``` ### GET /api/me/invites List every unredeemed, unexpired invitation addressed to the authenticated user's email, across both organization and stakeholder invites. Call this right after signup to show someone what is waiting for them. **Auth:** Required (JWT). No `X-Organization-Id` needed. ```bash curl -s https://api.invisible.app/api/me/invites \ -H "Authorization: Bearer " ``` **Response:** ```json { "invites": [ { "code": "aB3dE5gH7jK9mN1pQ2r", "kind": "organization", "organizationName": "Real Magic, Inc.", "inviterName": "Jes Lipson", "role": "viewer", "stakeholderName": null, "expiresAt": "2026-09-26T14:03:11Z" } ] } ``` ### POST /api/invites/{code}/accept Accept one invitation. Resolves organization and stakeholder invites through the same route, so the caller does not need to know which kind it holds. **Auth:** Required (JWT). No `X-Organization-Id` needed. ```bash curl -s -X POST https://api.invisible.app/api/invites/aB3dE5gH7jK9mN1pQ2r/accept \ -H "Authorization: Bearer " ``` **Response:** ```json { "accepted": true, "kind": "organization" } ``` Returns 400 if the invitation is invalid, expired, or already accepted. --- ## Cognito Admin Administrative endpoints for managing Cognito user accounts. ### GET /api/admin/cognito/users List Cognito users with confirmation and verification status. Optionally filter by email. **Auth:** Required. `owner` or `admin` role. **Query params:** | Param | Type | Description | |-------|------|-------------| | `email` | string | Filter by email address (partial match) | ```bash curl -s "https://api.invisible.app/api/admin/cognito/users?email=grant@example.com" \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "users": [ { "username": "44444444-0001-4000-8000-000000000003", "email": "grant@example.com", "userStatus": "CONFIRMED", "emailVerified": true, "enabled": true, "createdAt": "2026-03-15T10:00:00Z" } ] } ``` --- ### POST /api/admin/cognito/confirm Force-confirms an unverified Cognito user and sets their email as verified. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/admin/cognito/confirm \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "username": "grant@example.com" }' ``` **Response:** ```json { "confirmed": true, "message": "User grant@example.com confirmed and email marked as verified" } ``` --- ### POST /api/admin/cognito/resend-verification Resends the verification code to an unconfirmed user. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/admin/cognito/resend-verification \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "username": "grant@example.com" }' ``` **Response:** ```json { "sent": true, "message": "Verification code resent to grant@example.com" } ``` --- ## 6. Bulk Operations All bulk endpoints require `owner`, `admin`, or `member` role. ### POST /api/bulk/stakeholders Create multiple stakeholders in one call. **Auth:** Required. `owner`, `admin`, or `member` role. **Enum values for `type`:** `individual`, `entity`, `trust`, `fund`, `spv` ```bash curl -s -X POST https://api.invisible.app/api/bulk/stakeholders \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "stakeholders": [ { "name": "James Park", "type": "individual", "email": "james@acme.com", "jobTitle": "Senior Engineer", "department": "Engineering", "employeeId": "EMP-003" }, { "name": "Acme Ventures LLC", "type": "entity", "email": "legal@acmeventures.com" } ] }' ``` **Response:** ```json { "results": [ { "name": "James Park", "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "error": null }, { "name": "Acme Ventures LLC", "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000004", "error": null } ], "created": 2, "failed": 0 } ``` --- ### POST /api/bulk/transactions Record multiple transactions. Each transaction is committed individually (no preview step). Failures do not roll back successes. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X POST https://api.invisible.app/api/bulk/transactions \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "transactions": [ { "securityId": "CS-002", "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "securityClassId": "b2c3d4e5-0001-4000-8000-000000000001", "type": "issuance", "shares": 100000, "pricePerShare": 0.50, "transactionDate": "2024-03-01", "notes": "Early exercise of options", "confirm": true }, { "securityId": "SA-002", "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000004", "securityClassId": "b2c3d4e5-0001-4000-8000-000000000002", "type": "issuance", "shares": 500000, "pricePerShare": 1.50, "transactionDate": "2024-06-01", "notes": "Series A participation", "confirm": true } ] }' ``` **Response:** ```json { "results": [ { "securityId": "CS-002", "transactionId": "e5f6a7b8-0001-4000-8000-000000000003", "error": null }, { "securityId": "SA-002", "transactionId": "e5f6a7b8-0001-4000-8000-000000000004", "error": null } ], "created": 2, "failed": 0 } ``` --- ### POST /api/bulk/option-grants Create multiple option grants. Each grant is committed individually. Failures do not roll back successes. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X POST https://api.invisible.app/api/bulk/option-grants \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "grants": [ { "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "equityPlanId": "f6a7b8c9-0001-4000-8000-000000000001", "optionType": "iso", "sharesGranted": 50000, "exercisePrice": 0.50, "grantDate": "2024-03-01", "vestingStartDate": "2024-03-01", "expirationDate": "2034-03-01", "vestingScheduleId": "22222222-0001-4000-8000-000000000001", "status": "active", "category": "newHire", "confirm": true }, { "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000005", "equityPlanId": "f6a7b8c9-0001-4000-8000-000000000001", "optionType": "nqso", "sharesGranted": 25000, "exercisePrice": 0.50, "grantDate": "2024-04-01", "vestingStartDate": "2024-04-01", "expirationDate": "2034-04-01", "status": "active", "category": "advisory", "confirm": true } ] }' ``` **Response:** ```json { "results": [ { "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "grantId": "11111111-0001-4000-8000-000000000001", "error": null }, { "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000005", "grantId": "11111111-0001-4000-8000-000000000002", "error": null } ], "created": 2, "failed": 0 } ``` --- ## 7. Additional Write Endpoints ### POST /api/stakeholders Create a single stakeholder. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X POST https://api.invisible.app/api/stakeholders \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "James Park", "type": "individual", "email": "james@acme.com", "jobTitle": "Senior Engineer", "department": "Engineering", "employeeId": "EMP-003", "managerId": "a1b2c3d4-0001-4000-8000-000000000001" }' ``` **Response:** ```json { "stakeholderId": "a1b2c3d4-0001-4000-8000-000000000003" } ``` --- ### PUT /api/stakeholders/{id} Update stakeholder fields. Only provided fields are modified. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X PUT https://api.invisible.app/api/stakeholders/a1b2c3d4-0001-4000-8000-000000000003 \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "department": "Engineering", "jobTitle": "Staff Engineer", "displayName": "Friendly Name", "email": "user@company.com" }' ``` **Request fields (all optional):** | Field | Type | Description | |-------|------|-------------| | `name` | string | Full legal name | | `email` | string | Email address | | `department` | string | Department name | | `jobTitle` | string | Job title | | `displayName` | string | Friendly display name | | `employeeId` | string | Employee ID | | `managerId` | uuid | Manager stakeholder ID | **Response:** ```json { "updated": true } ``` Returns `404` if stakeholder not found. --- ### POST /api/stakeholders/merge Merges two duplicate stakeholders. Moves all grants, transactions, and beneficial ownership from source to target. Deletes the source stakeholder. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/stakeholders/merge \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "sourceStakeholderId": "a1b2c3d4-0001-4000-8000-000000000005", "targetStakeholderId": "a1b2c3d4-0001-4000-8000-000000000003" }' ``` **Response:** ```json { "merged": true, "grantsMoved": 3, "transactionsMoved": 5, "beneficialOwnershipMoved": 1, "message": "Stakeholder merged successfully. All records moved to target." } ``` --- ### POST /api/security-classes Create a new security class. **Auth:** Required. `owner`, `admin`, or `member` role. **Enum values for `type`:** `common`, `preferred`, `option`, `warrant`, `safe`, `convertibleNote` ```bash curl -s -X POST https://api.invisible.app/api/security-classes \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Series B Preferred", "type": "preferred", "originalIssuePrice": 5.00, "liquidationPreference": 1.0, "conversionRatio": 1.0, "isParticipating": false, "seniorityRank": 2, "authorizedShares": 5000000 }' ``` **Response:** ```json { "securityClassId": "b2c3d4e5-0001-4000-8000-000000000003" } ``` --- ### PUT /api/security-classes Update a security class. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X PUT https://api.invisible.app/api/security-classes \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "securityClassId": "b2c3d4e5-0001-4000-8000-000000000002", "authorizedShares": 4000000, "conversionRatio": 1.2, "notes": "Updated after anti-dilution adjustment" }' ``` **Response:** ```json { "updated": true } ``` --- ### POST /api/equity-plans Create a new equity incentive plan. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X POST https://api.invisible.app/api/equity-plans \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "2025 Equity Incentive Plan", "year": 2025, "authorizedShares": 1500000 }' ``` **Response:** ```json { "equityPlanId": "f6a7b8c9-0001-4000-8000-000000000002" } ``` --- ### PUT /api/equity-plans Update an equity plan. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X PUT https://api.invisible.app/api/equity-plans \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "equityPlanId": "f6a7b8c9-0001-4000-8000-000000000001", "authorizedShares": 1200000 }' ``` **Response:** ```json { "updated": true } ``` --- ### POST /api/investor-groups Create or update an investor group. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X POST https://api.invisible.app/api/investor-groups \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Series A Investors", "stakeholderIds": [ "a1b2c3d4-0001-4000-8000-000000000002", "a1b2c3d4-0001-4000-8000-000000000004" ] }' ``` To update an existing group, include `groupId`: ```bash curl -s -X POST https://api.invisible.app/api/investor-groups \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "groupId": "c3d4e5f6-0001-4000-8000-000000000001", "name": "Series A Investors", "stakeholderIds": [ "a1b2c3d4-0001-4000-8000-000000000002", "a1b2c3d4-0001-4000-8000-000000000004", "a1b2c3d4-0001-4000-8000-000000000006" ] }' ``` **Response:** ```json { "groupId": "c3d4e5f6-0001-4000-8000-000000000001" } ``` --- ## 8. MCP Integration The Invisible Cap Table exposes an MCP (Model Context Protocol) server for AI assistant integrations. **Endpoint:** `https://mcp.invisible.app/mcp` **Transport:** Streamable HTTP (stateless) **Auth:** OAuth 2.0 with PKCE. The authorization server metadata is at `https://api.invisible.app/.well-known/oauth-authorization-server`. MCP clients (Claude Desktop, Cursor, etc.) can discover the server and authenticate automatically using the standard MCP OAuth flow. The server exposes the same cap table read/write operations as the REST API as MCP tools. **How the tools are organized.** Reading goes through six tools, each with a required `view` parameter that picks the read: `get_cap_table_data`, `get_grant_data`, `get_valuation_data`, `get_signing_data`, `get_account_data`, and `get_support_data` (the Invisible support team only). Each tool's description lists its views and which parameters each view takes; a view the caller's role can't use is refused. Every change has its own descriptively named tool (`send_drafted_document`, `void_signature_request`, `record_transaction`), so a client's approval prompt says exactly what will happen. Tools carry MCP annotations: the six read tools are `readOnlyHint` (clients such as ChatGPT don't ask for approval to read), and tools that delete, void, cancel, revoke, merge or remove are `destructiveHint`. If your client cached the old tool list (individual `get_*` and `list_*` read tools, removed 2026-10-04), refresh or reconnect it. ### Bulk tool inputs The bulk MCP tools (`bulk_create_stakeholders`, `bulk_record_transactions`, `bulk_create_option_grants`, `bulk_set_managers`) take their collection as a JSON string. Either shape is accepted: ```jsonc // a bare array [{ "stakeholderId": "...", "managerId": "..." }] // or an object wrapping it, matching the REST body { "assignments": [{ "stakeholderId": "...", "managerId": "..." }] } ``` The wrapping property is `stakeholders`, `transactions`, `grants`, and `assignments` respectively. The REST endpoints themselves always take the wrapped object. Large batches are applied one row at a time inside a single request, so keep a call to roughly 50 items to stay within the API Gateway timeout. Each row reports its own outcome in `results`, and one failed row does not abort the rest. ### Tool errors A tool that fails returns an MCP error result whose content is the same JSON shape the REST API uses: ```json { "error": "Manager must be in the same organization." } ``` Validation and business-rule messages are passed through so a caller can correct the request. Unexpected server-side faults return a generic message and are logged server-side rather than exposed. --- ## 9. 409A Valuations ### POST /api/409a/start Start a new 409A valuation. Creates a draft report with initial inputs. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/409a/start \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "valuationDate": "2026-03-31", "companyDescription": "Series B SaaS company, $8M ARR, 45% YoY growth", "industry": "Enterprise Software / SaaS", "financials": { "revenue": 8000000, "revenueGrowthRate": 0.45, "ebitda": -1200000, "cashBalance": 12000000, "totalDebt": 0, "projectedRevenue": [12000000, 18000000, 26000000, 35000000, 42000000] }, "dcfAssumptions": { "discountRate": 0.30, "terminalGrowthRate": 0.03, "projectionYears": 5 }, "comparableCompanies": [ { "name": "Comparable A", "evRevenue": 12.5, "evEbitda": null }, { "name": "Comparable B", "evRevenue": 9.8, "evEbitda": 45.0 }, { "name": "Comparable C", "evRevenue": 15.2, "evEbitda": null } ], "methodologyWeights": { "dcf": 0.30, "marketComps": 0.40, "opmBacksolve": 0.30 } }' ``` **Response:** ```json { "reportId": "99999999-0001-4000-8000-000000000001" } ``` --- ### GET /api/409a/reports List all 409A valuation reports for the organization. **Auth:** Required. ```bash curl -s https://api.invisible.app/api/409a/reports \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "reports": [ { "id": "99999999-0001-4000-8000-000000000001", "valuationDate": "2026-03-31", "status": "draft", "fairMarketValue": null, "reviewerUserId": null, "createdAt": "2026-03-31T14:00:00Z", "finalizedAt": null } ] } ``` --- ### GET /api/409a/reports/{id} Full report detail including inputs, calculated values, equity allocation, and review status. **Auth:** Required. ```bash curl -s https://api.invisible.app/api/409a/reports/99999999-0001-4000-8000-000000000001 \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "id": "99999999-0001-4000-8000-000000000001", "valuationDate": "2026-03-31", "status": "computed", "inputs": { "companyDescription": "Series B SaaS company, $8M ARR, 45% YoY growth", "industry": "Enterprise Software / SaaS", "financials": { "revenue": 8000000, "revenueGrowthRate": 0.45, "ebitda": -1200000 }, "dcfAssumptions": { "discountRate": 0.30, "terminalGrowthRate": 0.03, "projectionYears": 5 }, "comparableCompanies": [ "..." ], "methodologyWeights": { "dcf": 0.30, "marketComps": 0.40, "opmBacksolve": 0.30 } }, "calculations": { "dcfValue": 85000000, "marketCompsValue": 96000000, "opmBacksolveValue": 91000000, "weightedEnterpriseValue": 91500000, "dlomDiscount": 0.25, "fairMarketValuePerShare": 1.85 }, "allocation": { "enterpriseValue": 91500000, "commonEquityValue": 18500000, "commonSharesOutstanding": 10000000, "preDlomPerShare": 1.85, "dlom": 0.25, "postDlomPerShare": 1.39 }, "reviewerUserId": null, "reviewNotes": null, "reviewApproved": null, "createdAt": "2026-03-31T14:00:00Z", "finalizedAt": null } ``` --- ### PUT /api/409a/reports/{id}/inputs Update the inputs on a draft or computed report (financials, comparable companies, assumptions). Cannot update a finalized report. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X PUT https://api.invisible.app/api/409a/reports/99999999-0001-4000-8000-000000000001/inputs \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "financials": { "revenue": 8500000, "revenueGrowthRate": 0.48, "ebitda": -800000, "cashBalance": 11000000, "totalDebt": 0, "projectedRevenue": [13000000, 19500000, 28000000, 37000000, 44000000] }, "comparableCompanies": [ { "name": "Comparable A", "evRevenue": 13.0, "evEbitda": null }, { "name": "Comparable B", "evRevenue": 10.2, "evEbitda": 42.0 }, { "name": "Comparable C", "evRevenue": 14.8, "evEbitda": null }, { "name": "Comparable D", "evRevenue": 11.5, "evEbitda": 50.0 } ] }' ``` **Response:** ```json { "updated": true } ``` Returns `400` if the report is finalized. --- ### POST /api/409a/reports/{id}/compute Run the full valuation computation: DCF, Market Comps, OPM Backsolve, and DLOM calculation. Updates the report with calculated values. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/409a/reports/99999999-0001-4000-8000-000000000001/compute \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "computed": true, "fairMarketValuePerShare": 1.39, "enterpriseValue": 91500000, "methodResults": { "dcf": 85000000, "marketComps": 96000000, "opmBacksolve": 91000000 }, "dlomDiscount": 0.25 } ``` --- ### POST /api/409a/reports/{id}/assign-reviewer Assign a reviewer to a 409A report. The reviewer must be a member of the organization. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/409a/reports/99999999-0001-4000-8000-000000000001/assign-reviewer \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "reviewerUserId": "44444444-0001-4000-8000-000000000002" }' ``` **Response:** ```json { "assigned": true } ``` --- ### POST /api/409a/reports/{id}/review Submit a review for a 409A report. Only the assigned reviewer can call this endpoint. **Auth:** Required. Must be the assigned reviewer. ```bash curl -s -X POST https://api.invisible.app/api/409a/reports/99999999-0001-4000-8000-000000000001/review \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "notes": "Methodology and assumptions look reasonable. Comparable set is appropriate for this stage and industry. DLOM within expected range.", "approve": true }' ``` **Response:** ```json { "reviewed": true, "approved": true } ``` Returns `403` if the caller is not the assigned reviewer. --- ### POST /api/409a/reports/{id}/finalize Lock the report as final. Once finalized, no further edits are allowed. The fair market value is recorded for use in option pricing. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/409a/reports/99999999-0001-4000-8000-000000000001/finalize \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "finalized": true, "fairMarketValuePerShare": 1.39, "valuationDate": "2026-03-31" } ``` Returns `400` if the report has not been computed yet. --- ## 10. SBC Expensing ### POST /api/sbc/valuations Record a 409A valuation with Black-Scholes assumptions for SBC expense calculations. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/sbc/valuations \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "valuationDate": "2026-03-31", "fairMarketValue": 1.39, "expectedTermYears": 6.25, "riskFreeRate": 0.042, "volatility": 0.55, "dividendYield": 0.0, "notes": "Based on Q1 2026 409A valuation, volatility from public SaaS peer group" }' ``` **Response:** ```json { "sbcValuationId": "aaaaaaaa-0001-4000-8000-000000000001" } ``` --- ### GET /api/sbc/valuations List all SBC valuations (409A snapshots with Black-Scholes assumptions). **Auth:** Required. ```bash curl -s https://api.invisible.app/api/sbc/valuations \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "valuations": [ { "id": "aaaaaaaa-0001-4000-8000-000000000001", "valuationDate": "2026-03-31", "fairMarketValue": 1.39, "expectedTermYears": 6.25, "riskFreeRate": 0.042, "volatility": 0.55, "dividendYield": 0.0, "notes": "Based on Q1 2026 409A valuation", "createdAt": "2026-03-31T15:00:00Z" } ] } ``` --- ### POST /api/sbc/compute Compute Black-Scholes fair values and expense schedules for all active option grants, using the most recent SBC valuation assumptions. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/sbc/compute \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "computed": true, "grantsProcessed": 12, "totalFairValue": 485000.00, "totalUnamortizedExpense": 312000.00, "summary": { "iso": { "grants": 8, "fairValue": 350000.00 }, "nqso": { "grants": 4, "fairValue": 135000.00 } } } ``` --- ### GET /api/sbc/expense-schedule Monthly SBC expense schedule with optional filters. **Auth:** Required. **Query params:** | Param | Type | Description | |-------|------|-------------| | `startDate` | string | Start date (`YYYY-MM-DD`) | | `endDate` | string | End date (`YYYY-MM-DD`) | | `department` | string | Filter by department | | `grantId` | uuid | Filter by specific grant | ```bash curl -s "https://api.invisible.app/api/sbc/expense-schedule?startDate=2026-01-01&endDate=2026-12-31&department=Engineering" \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "schedule": [ { "month": "2026-01", "totalExpense": 12500.00, "byDepartment": { "Engineering": 8200.00, "Product": 2800.00, "Sales": 1500.00 }, "byGrantType": { "iso": 9000.00, "nqso": 3500.00 } }, { "month": "2026-02", "totalExpense": 13100.00, "byDepartment": { "Engineering": 8600.00, "Product": 2900.00, "Sales": 1600.00 }, "byGrantType": { "iso": 9400.00, "nqso": 3700.00 } } ], "totalForPeriod": 155000.00 } ``` --- ### GET /api/sbc/disclosure ASC 718 disclosure data for a fiscal year. Returns the information needed for stock-based compensation footnotes in financial statements. **Auth:** Required. **Query params:** | Param | Type | Description | |-------|------|-------------| | `fiscalYearStart` | string | Fiscal year start date (`YYYY-MM-DD`) | | `fiscalYearEnd` | string | Fiscal year end date (`YYYY-MM-DD`) | ```bash curl -s "https://api.invisible.app/api/sbc/disclosure?fiscalYearStart=2025-01-01&fiscalYearEnd=2025-12-31" \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "fiscalYear": { "start": "2025-01-01", "end": "2025-12-31" }, "totalSbcExpense": 185000.00, "grantActivity": { "outstandingBeginning": { "shares": 450000, "weightedAvgExercisePrice": 0.65 }, "granted": { "shares": 120000, "weightedAvgExercisePrice": 1.39 }, "exercised": { "shares": 30000, "weightedAvgExercisePrice": 0.50 }, "forfeited": { "shares": 15000, "weightedAvgExercisePrice": 0.75 }, "outstandingEnd": { "shares": 525000, "weightedAvgExercisePrice": 0.82 }, "exercisableEnd": { "shares": 210000, "weightedAvgExercisePrice": 0.60 } }, "weightedAvgAssumptions": { "expectedTermYears": 6.25, "riskFreeRate": 0.042, "volatility": 0.55, "dividendYield": 0.0 }, "weightedAvgGrantDateFairValue": 0.92, "unamortizedExpense": 312000.00, "weightedAvgRemainingVestingYears": 2.8 } ``` --- ## Visibility & Org Hierarchy These endpoints manage manager relationships and department-based visibility. All require `owner` or `admin` role. ### PUT /api/stakeholders/{id}/manager Set or clear a stakeholder's reporting manager. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X PUT https://api.invisible.app/api/stakeholders/a1b2c3d4-0001-4000-8000-000000000003/manager \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "managerId": "a1b2c3d4-0001-4000-8000-000000000001" }' ``` Pass `"managerId": null` to clear the manager. Returns `400` if the assignment would create a circular reference. **Response:** ```json { "updated": true } ``` --- ### POST /api/stakeholders/bulk-set-managers Set manager relationships for multiple stakeholders at once. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/stakeholders/bulk-set-managers \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "assignments": [ { "stakeholderId": "...", "managerId": "..." }, { "stakeholderId": "...", "managerId": "..." } ] }' ``` **Response:** ```json { "results": [ { "stakeholderId": "...", "success": true, "error": null }, { "stakeholderId": "...", "success": false, "error": "Would create circular reference" } ], "updated": 1, "failed": 1 } ``` --- ### POST /api/department-access Grant a user view access to all equity data in specified departments. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/department-access \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "c3d4e5f6-0001-4000-8000-000000000001", "departments": ["Sales", "Marketing"] }' ``` **Response:** ```json { "granted": 2 } ``` --- ### POST /api/department-access/revoke Revoke department-based view access. **Auth:** Required. `owner` or `admin` role. ```bash curl -s -X POST https://api.invisible.app/api/department-access/revoke \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "c3d4e5f6-0001-4000-8000-000000000001", "departments": ["Marketing"] }' ``` **Response:** ```json { "revoked": 1 } ``` --- ### GET /api/department-access List all department access grants. **Auth:** Required. `owner` or `admin` role. **Query params:** | Param | Type | Description | |-------|------|-------------| | `userId` | uuid | Filter to a specific user | ```bash curl -s "https://api.invisible.app/api/department-access?userId=c3d4e5f6-0001-4000-8000-000000000001" \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "departmentAccess": [ { "id": "...", "userId": "c3d4e5f6-0001-4000-8000-000000000001", "department": "Sales", "grantedBy": "...", "createdAt": "2026-04-06T00:00:00Z" } ] } ``` --- ## Beneficial Ownership Track beneficial ownership relationships (who owns what percentage of an entity stakeholder). ### POST /api/beneficial-owners Create or update a beneficial ownership record. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X POST https://api.invisible.app/api/beneficial-owners \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "entityStakeholderId": "a1b2c3d4-0001-4000-8000-000000000004", "ownerStakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "ownershipPct": 3.0055, "notes": "Direct ownership through LLC" }' ``` **Response:** ```json { "id": "bbbbbbbb-0001-4000-8000-000000000001" } ``` --- ### GET /api/beneficial-owners List beneficial ownership records. Filter by entity or owner. **Auth:** Required. Any authenticated user. **Query params:** | Param | Type | Description | |-------|------|-------------| | `entityStakeholderId` | uuid | Filter by entity stakeholder | | `ownerStakeholderId` | uuid | Filter by owner stakeholder | ```bash curl -s "https://api.invisible.app/api/beneficial-owners?entityStakeholderId=a1b2c3d4-0001-4000-8000-000000000004" \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "beneficialOwners": [ { "id": "bbbbbbbb-0001-4000-8000-000000000001", "entityStakeholderId": "a1b2c3d4-0001-4000-8000-000000000004", "entityName": "Real Magic LLC", "ownerStakeholderId": "a1b2c3d4-0001-4000-8000-000000000003", "ownerName": "Grant Kitching", "ownershipPct": 3.0055, "sharesEquivalent": 180330 } ] } ``` --- ### DELETE /api/beneficial-owners/{entityId}/{ownerId} Remove a beneficial ownership record. **Auth:** Required. `owner`, `admin`, or `member` role. ```bash curl -s -X DELETE https://api.invisible.app/api/beneficial-owners/a1b2c3d4-0001-4000-8000-000000000004/a1b2c3d4-0001-4000-8000-000000000003 \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json { "removed": true } ``` --- ## Invisible Sign (E-Signature) Send documents for e-signature, track them, and download the sealed result. Kinds: `grant_agreement`, `stock_certificate`, `drafted_document` (written by the assistant and sent from a draft, see Drafts below), and `uploaded` (a PDF you upload, with fields placed by Invisible, see Uploaded documents below). For `grant_agreement`, Invisible generates the stock option grant agreement from the grant and places every field itself. If the org has an option signatory, they sign first (`routingOrder` 1), then the optionee. Signers sign at `https://invisible.app/sign/{token}` from their email; they need no account. Each recipient has an `action`: `sign`, or `receive_copy` for someone who gets the sealed document at completion without signing (the holder of a stock certificate). Status values are snake_case strings. Request: `draft`, `sent`, `in_progress`, `completed`, `declined`, `voided`, `expired`. Recipient: `pending` (not emailed yet, waiting for an earlier signer), `sent`, `viewed`, `consented`, `signed`, `declined`. All `/api/sign/*` endpoints require authentication and the `owner` or `admin` role. ### POST /api/sign/requests Generate a document and send it for signature. ```bash curl -s -X POST https://api.invisible.app/api/sign/requests \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "grant_agreement", "grantId": "a1b2c3d4-0002-4000-8000-000000000042", "message": "Welcome aboard!" }' ``` **Response:** ```json { "message": "Sent \"Stock Option Grant Agreement for Priya Shah (#42)\" to Jesse Lipson. Priya Shah will get it after that. The link expires 2026-10-31.", "request": { "id": "e48f3833-167d-49a8-ae58-b9e4132c1c7e", "title": "Stock Option Grant Agreement for Priya Shah (#42)", "status": "sent", "orderMode": "sequential", "sourceKind": "grant_agreement", "sourceRef": "a1b2c3d4-0002-4000-8000-000000000042", "createdAt": "2026-10-01T14:51:41Z", "sentAt": "2026-10-01T14:51:41Z", "expiresAt": "2026-10-31T14:51:41Z", "hasSignedDocument": false, "recipients": [ { "name": "Jesse Lipson", "email": "jesse@example.com", "roleLabel": "Company", "routingOrder": 1, "status": "sent", "signedAt": null }, { "name": "Priya Shah", "email": "priya@example.com", "roleLabel": "Optionee", "routingOrder": 2, "status": "pending", "signedAt": null } ], "evidence": [ { "event": "created", "recipient": null, "occurredAt": "2026-10-01T14:51:41Z", "ipAddress": null }, { "event": "sent", "recipient": "Jesse Lipson", "occurredAt": "2026-10-01T14:51:41Z", "ipAddress": null } ] } } ``` **Errors (400):** grant not found; grant already accepted or declined; stakeholder has no email on file; an open request already exists for this grant (resend or void it instead). **Stock certificates:** send `{ "kind": "stock_certificate", "certificateNumber": "PA-3" }` or `{ "kind": "stock_certificate", "stakeholderId": "..." }` for all of a stakeholder's common and preferred holdings. The two certificate signatories (`/api/sign/settings/certificates`) sign in order; the holder is a copy recipient (`action: "receive_copy"`) emailed the sealed certificate at completion. A holding that already has a completed certificate is re-sent to the holder. `message` lists one line per certificate. --- ### GET /api/sign/requests List requests, newest first. Optional `status` and `kind` query filters. ```bash curl -s "https://api.invisible.app/api/sign/requests?status=in_progress" \ -H "Authorization: Bearer icap_YOUR_KEY" ``` **Response:** ```json [ { "id": "e48f3833-167d-49a8-ae58-b9e4132c1c7e", "title": "Stock Option Grant Agreement for Priya Shah (#42)", "status": "in_progress", "sourceKind": "grant_agreement", "sourceRef": "a1b2c3d4-0002-4000-8000-000000000042", "recipientCount": 2, "signedCount": 1, "createdAt": "2026-10-01T14:51:41Z", "sentAt": "2026-10-01T14:51:41Z", "completedAt": null, "expiresAt": "2026-10-31T14:51:41Z" } ] ``` --- ### GET /api/sign/requests/{id} One request with recipients and the evidence trail (same shape as `request` above). --- ### POST /api/sign/requests/{id}/resend Email everyone who was sent the request but has not finished, with fresh links, and extend the expiry. Their earlier links stop working. ```bash curl -s -X POST https://api.invisible.app/api/sign/requests/e48f3833-167d-49a8-ae58-b9e4132c1c7e/resend \ -H "Authorization: Bearer icap_YOUR_KEY" ``` --- ### POST /api/sign/requests/{id}/void Void an open request so its links stop working. ```bash curl -s -X POST https://api.invisible.app/api/sign/requests/e48f3833-167d-49a8-ae58-b9e4132c1c7e/void \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "reason": "Wrong share count; reissuing" }' ``` --- ### GET /api/sign/requests/{id}/document The sealed, signed PDF (with its certificate of completion). `404` until everyone has signed. ```bash curl -s -o signed.pdf https://api.invisible.app/api/sign/requests/e48f3833-167d-49a8-ae58-b9e4132c1c7e/document \ -H "Authorization: Bearer icap_YOUR_KEY" ``` --- ### GET /api/sign/settings/reminders ```json { "settings": { "remindersEnabled": true, "reminderIntervalDays": 3, "reminderMax": 3, "grantAcceptanceReminders": false, "grantAcceptanceIntervalDays": 7, "grantAcceptanceMax": 3 }, "summary": "Signers are reminded every 3 days, up to 3 times, until they sign. Employees with grants waiting to be accepted are not reminded automatically. Changes apply to documents sent from now on." } ``` Signer reminders go to whoever's turn it is on open, unexpired requests, as a fresh link (earlier links stop working); they never extend the expiry. Grant-acceptance reminders cover the cap table's typed-name acceptance flow: linked employees get a reminder to accept, others the account-setup invite. Reminders run hourly. ### PUT /api/sign/settings/reminders Only the fields sent change. Intervals 1 to 30 days; limits 1 to 10. Applies to requests sent from then on. ```bash curl -s -X PUT https://api.invisible.app/api/sign/settings/reminders \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "intervalDays": 2, "maxReminders": 5, "grantAcceptanceReminders": true }' ``` ### POST /api/sign/requests/{id}/reminders Turns reminders on or off for one open request, or changes its schedule: `{ "enabled": false }` or `{ "intervalDays": 7, "maxReminders": 2 }`. Returns `{ "message", "request" }`. Request views include `reminders: { "enabled", "intervalDays", "max" }` and, per recipient, `remindersSent`; reminders appear in `evidence` as `reminded`. ### GET /api/sign/settings/option-signatory ```json { "signatory": { "userId": "u1111111-0000-4000-8000-000000000001", "name": "Jesse Lipson", "email": "jesse@example.com", "title": "Chief Executive Officer" } } ``` `signatory` is `null` when none is set. ### PUT /api/sign/settings/option-signatory Set who countersigns grant agreements for the company. The user must be an owner or admin of the org. Send `"userId": null` to clear it. ```bash curl -s -X PUT https://api.invisible.app/api/sign/settings/option-signatory \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "u1111111-0000-4000-8000-000000000001", "title": "Chief Executive Officer" }' ``` --- ### GET /api/sign/settings/certificates ```json { "signatory1": { "userId": "u1111111-0000-4000-8000-000000000001", "name": "Jesse Lipson", "email": "jesse@example.com", "title": "Chief Executive Officer" }, "signatory2": { "userId": "u2222222-0000-4000-8000-000000000002", "name": "Robert Mills", "email": "robert@example.com", "title": "Secretary" }, "stateOfIncorporation": "Delaware", "parValuePerShare": 0.00001, "legend": null, "ready": true } ``` ### PUT /api/sign/settings/certificates Only the fields sent change. The two signatories must be different owners or admins of the org. `legend` replaces the standard Securities Act legend; `"clearLegend": true` goes back to it. ```bash curl -s -X PUT https://api.invisible.app/api/sign/settings/certificates \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "signatory1UserId": "u1111111-0000-4000-8000-000000000001", "signatory1Title": "Chief Executive Officer", "signatory2UserId": "u2222222-0000-4000-8000-000000000002", "signatory2Title": "Secretary", "stateOfIncorporation": "Delaware", "parValuePerShare": 0.00001 }' ``` --- ### Drafts: POST /api/sign/drafts Renders a document from structured content and stores it as a draft with every signature, name, title and date field placed (one signature block per party). Nothing is sent. Pass `?draftId=` to revise an unsent draft in place. A party with `isEntity: true` gets By, Name and Title lines, prefilled from `signerName` and `signerTitle` when given; a person gets Signature and Name. Every party gets a Date line, filled in at signing. ```bash curl -s -X POST https://api.invisible.app/api/sign/drafts \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Mutual Non-Disclosure Agreement", "kind": "mutual_nda", "sections": [ { "heading": "1. Parties", "paragraphs": ["This Agreement is between Real Magic Inc. and Northwind Analytics, Inc."] }, { "heading": "2. Confidential Information", "paragraphs": ["Each party will keep the other party'"'"'s Confidential Information confidential for two years."] } ], "parties": [ { "role": "Northwind", "name": "Northwind Analytics, Inc.", "isEntity": true }, { "role": "Real Magic", "name": "Real Magic Inc.", "isEntity": true, "signerName": "Jesse Lipson", "signerTitle": "Chief Executive Officer" } ] }' ``` Response: ```json { "message": "Drafted \"Mutual Non-Disclosure Agreement\" (2 pages), with signature blocks for Northwind, Real Magic. Nothing has been sent.", "draft": { "id": "d1111111-0000-4000-8000-000000000001", "title": "Mutual Non-Disclosure Agreement", "kind": "mutual_nda", "pageCount": 2, "roles": ["Northwind", "Real Magic"], "updatedAt": "2026-10-04T15:02:11Z", "sentRequestId": null } } ``` Limits: 1 to 6 parties, each with its own role; 1 to 60 sections; paragraphs under 6,000 characters. ### GET /api/sign/drafts Drafts, newest first. Unsent only by default; `?includeSent=true` for all. Returns `{ "drafts": [ , ... ] }`. ### GET /api/sign/drafts/{id} `{ "message", "draft", "content", "upload", "fields" }`. For a drafted document, `content` is the structured content last rendered (send it back to POST /api/sign/drafts with `?draftId=` to revise). For an uploaded document, `upload` has the file name, `normalization` (`as-is` or `copied`), the signer roles and whether fields are placed, and `fields` lists them numbered as in the preview. `?includeSpots=true` adds `upload.spots`: every place a field can be added (`id`, `page`, `kind`, `rect`, `context`). ### GET /api/sign/drafts/{id}/document The draft PDF exactly as it will be sent; for an uploaded document with fields placed, the preview with every field numbered and colored by signer. ### Uploaded documents: POST /api/sign/uploads The PDF is the request body (`Content-Type: application/pdf`), up to 4.2 MB and 50 pages; `?fileName=` names it. Creates a draft of kind `uploaded`. A PDF with edit restrictions (owner password) is copied into a plain PDF so it can be signed (`normalization: "copied"`); one that needs a password to open is refused. ```bash curl -s -X POST "https://api.invisible.app/api/sign/uploads?fileName=engagement-letter.pdf" \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/pdf" \ --data-binary @engagement-letter.pdf ``` Response: `{ "message": "Uploaded \"engagement-letter.pdf\" (2 pages).", "draft": { "id": "...", "kind": "uploaded", "roles": [], ... } }` ### POST /api/sign/drafts/{id}/prepare Places the fields on an uploaded document. Invisible extracts every place a mark could go (drawn lines, typed underscores, table cells, boxes, form widgets, blank space after a label; scanned pages from the image) and a model chooses which are fields, of what type, and for which signer. The whole document is completed for the signers, forms included, skipping what is already filled in and options already chosen. Body: `{ "signers": ["Client", "Accountant"], "note": "optional guidance" }`; omit `signers` to read them from the document. Running it again starts over. Response: `{ "message", "draft", "fields": [ { "number": 1, "page": 2, "type": "signature", "role": "Client", "label": "under: Client signature" }, ... ] }`. Fetch the numbered preview from `GET /api/sign/drafts/{id}/document`. ### POST /api/sign/drafts/{id}/fields Corrects fields by the numbers in the preview; they are renumbered after. ```json { "remove": [4], "change": [ { "field": 2, "role": "Client", "type": "date_signed" } ], "add": [ { "spot": "p2c14", "type": "signature", "role": "Client" } ] } ``` `type` is one of `signature`, `initials`, `date_signed`, `text`, `checkbox`. Spot ids come from `GET /api/sign/drafts/{id}?includeSpots=true`. A new role name becomes a signer; a signer left with no fields is dropped. Send with `POST /api/sign/drafts/{id}/send` as for drafted documents; every signer needs at least one signature or initials field. ### POST /api/sign/drafts/{id}/send Sends the draft as previewed. Name one recipient per party, by the party's `role`: a `name` and `email`, or `"me": true` for the signed-in user. `order` is `parallel` (default) or `sequential` (in the parties' order). A draft is sent once; afterwards `sentRequestId` points at the signature request (kind `drafted_document`) and the draft can't be revised. ```bash curl -s -X POST https://api.invisible.app/api/sign/drafts/d1111111-0000-4000-8000-000000000001/send \ -H "Authorization: Bearer icap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "recipients": [ { "role": "Northwind", "name": "Dana Lee", "email": "dana@northwind.com" }, { "role": "Real Magic", "me": true } ], "order": "sequential", "message": "Here is the NDA we discussed." }' ``` Response: `{ "message": "Sent \"Mutual Non-Disclosure Agreement\" to Dana Lee. Jesse Lipson will get it after that. ...", "request": }`. Errors (`400`): a role that isn't a party, a party with no recipient, a missing or invalid email, or a draft already sent. --- ### POST /api/sign/sweep Retry completion (stamp, certificate, seal, store, notify) for any request where everyone signed but completion did not finish. Returns `{ "completed": }`. --- ### Signer endpoints: /api/public/sign/{token} Used by the signer page. No authentication: the token from the signing email is the credential. Rate limited per client address (429 when exceeded). Responses carry `Referrer-Policy: no-referrer` and `Cache-Control: no-store`. | Method | Path | Body | Purpose | |---|---|---|---| | GET | `/api/public/sign/{token}` | | Signer view: title, sender, organization, this recipient's fields (points, top-left origin), consent text and version, `consented`, `consentOnFileSince` (when the signer's standing consent for this organization was given, or null), other recipients' first names and statuses, and `blocked` (why they can't act, or null). Opening the view with standing consent on file moves the recipient straight to `consented` and logs a `consent_on_file` evidence event | | GET | `/api/public/sign/{token}/document` | | The PDF: the source document while signing is open, the sealed copy once complete | | POST | `/api/public/sign/{token}/consent` | `{ "version": "invisible-esign-consent-v2" }` | Record ESIGN consent. v2 is standing consent: it covers every document this organization sends the signer (matched by email) until withdrawn | | POST | `/api/public/sign/{token}/withdraw-consent` | | Withdraw the signer's standing consent for this organization. If they hadn't signed this request yet, it returns to the consent step | | POST | `/api/public/sign/{token}/sign` | `{ "adoptedName", "signatureKind": "drawn"\|"typed", "signaturePngBase64", "values": [{ "fieldId", "value" }] }` | Sign. Validates required fields; the date is filled in by the server | | POST | `/api/public/sign/{token}/decline` | `{ "reason" }` | Decline; closes the request and notifies the sender | Action responses: `{ "ok": true, "message": "...", "completed": false }`, or `400` with `ok: false` and a message to show the signer. --- ## Error Responses All errors follow this format: ```json { "error": "Description of what went wrong" } ``` | Status | Meaning | |--------|---------| | 400 | Bad request (missing fields, invalid data) | | 401 | Missing/invalid auth token | | 403 | Insufficient permissions for this action | | 404 | Resource not found |