> ## Documentation Index
> Fetch the complete documentation index at: https://developer.vanta.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload a document

> Attach an evidence file to a Vanta document and submit it for review, using the Manage Vanta API.

export function vibePromptBuildText(prompt, pageTitle) {
  const url = typeof window !== "undefined" && window.location ? window.location.href : "";
  const ref = url ? "\n\nReference: " + (pageTitle || "Vanta docs") + " — " + url : "";
  return prompt + ref;
}

export function vibePromptOpen(e) {
  e.preventDefault();
  e.stopPropagation();
  const link = e.currentTarget;
  const target = link.getAttribute("data-target");
  const prompt = link.getAttribute("data-prompt") || "";
  const pageTitle = link.getAttribute("data-page-title") || "";
  const URL_LIMIT = 8000;
  const text = vibePromptBuildText(prompt, pageTitle);
  let enc = encodeURIComponent(text);
  if (enc.length > URL_LIMIT) enc = encodeURIComponent(text.slice(0, 2400));
  let dest = "";
  if (target === "cursor") dest = "cursor://anysphere.cursor-deeplink/prompt?text=" + enc; else if (target === "claude") dest = "https://claude.ai/new?q=" + enc; else if (target === "chatgpt") dest = "https://chatgpt.com/?q=" + enc;
  if (!dest) return;
  if (target === "cursor") {
    window.location.href = dest;
  } else {
    window.open(dest, "_blank", "noopener,noreferrer");
  }
}

export function vibePromptCopy(e) {
  e.preventDefault();
  e.stopPropagation();
  const btn = e.currentTarget;
  const prompt = btn.getAttribute("data-prompt") || "";
  const pageTitle = btn.getAttribute("data-page-title") || "";
  const original = btn.getAttribute("data-label") || btn.innerText;
  btn.setAttribute("data-label", original);
  const text = vibePromptBuildText(prompt, pageTitle);
  navigator.clipboard.writeText(text).then(function () {
    btn.innerText = "Copied";
    setTimeout(function () {
      btn.innerText = original;
    }, 1500);
  }, function () {
    btn.innerText = "Copy failed";
    setTimeout(function () {
      btn.innerText = original;
    }, 1500);
  });
}

export function vibePromptToggle(e) {
  e.preventDefault();
  const summary = e.currentTarget;
  const panel = summary.parentElement;
  if (!panel) return;
  const body = panel.querySelector(".vibe-prompt__body");
  const chevron = summary.querySelector(".vibe-prompt__chevron");
  const isOpen = panel.getAttribute("data-open") === "true";
  const next = isOpen ? "false" : "true";
  panel.setAttribute("data-open", next);
  summary.setAttribute("aria-expanded", next);
  if (body) body.style.display = isOpen ? "none" : "block";
  if (chevron) chevron.style.transform = isOpen ? "rotate(0deg)" : "rotate(180deg)";
}

export const BuildPrompt = ({prompt, pageTitle, defaultOpen}) => <div className="vibe-prompt__panel" data-open={defaultOpen ? "true" : "false"} style={{
  marginTop: "0.85rem",
  borderRadius: "12px",
  border: "1px solid var(--vanta-border, rgba(120, 120, 130, 0.18))",
  background: "color-mix(in srgb, #5E05C4 4%, transparent)",
  overflow: "hidden"
}}>
    <button type="button" onClick={vibePromptToggle} aria-expanded={defaultOpen ? "true" : "false"} style={{
  display: "flex",
  alignItems: "center",
  gap: "0.75rem",
  width: "100%",
  padding: "0.9rem 1.1rem",
  background: "transparent",
  border: "none",
  textAlign: "left",
  cursor: "pointer",
  font: "inherit",
  color: "inherit"
}}>
      <span style={{
  display: "inline-flex",
  alignItems: "center",
  justifyContent: "center",
  gap: "0.35rem",
  padding: "0.2rem 0.55rem",
  borderRadius: "9999px",
  fontSize: "0.7rem",
  fontWeight: 700,
  letterSpacing: "0.06em",
  textTransform: "uppercase",
  color: "#5E05C4",
  background: "color-mix(in srgb, #5E05C4 12%, transparent)",
  border: "1px solid color-mix(in srgb, #5E05C4 30%, transparent)",
  whiteSpace: "nowrap",
  flexShrink: 0,
  minWidth: "9rem"
}}>
        Code this
      </span>
      <span style={{
  flex: 1,
  minWidth: 0
}}>
        <span className="text-gray-600 dark:text-gray-300" style={{
  display: "block",
  fontSize: "0.85rem",
  lineHeight: 1.45
}}>
          Generate a script or app that performs this function using the Vanta API.
        </span>
      </span>
      <span className="vibe-prompt__chevron" aria-hidden="true" style={{
  flexShrink: 0,
  display: "inline-flex",
  alignItems: "center",
  justifyContent: "center",
  width: "24px",
  height: "24px",
  color: "#5E05C4",
  transform: defaultOpen ? "rotate(180deg)" : "rotate(0deg)",
  transition: "transform 200ms ease"
}}>
        <Icon icon="chevron-down" iconType="regular" size={14} color="#5E05C4" />
      </span>
    </button>
    <div className="vibe-prompt__body" style={{
  display: defaultOpen ? "block" : "none",
  padding: "0 1.1rem 1.1rem"
}}>
      <div style={{
  margin: 0,
  padding: "0.85rem 1rem",
  borderRadius: "8px",
  background: "rgba(15, 17, 21, 0.92)",
  color: "#f4f4f5",
  fontSize: "0.78rem",
  lineHeight: 1.55,
  fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
  whiteSpace: "pre-wrap",
  wordBreak: "break-word",
  overflowWrap: "anywhere",
  overflowX: "hidden",
  overflowY: "auto",
  maxHeight: "22rem"
}}>
        {prompt}
      </div>
      <div className="not-prose" style={{
  display: "flex",
  flexWrap: "wrap",
  gap: "0.5rem",
  marginTop: "0.75rem"
}}>
        <button type="button" onClick={vibePromptCopy} data-prompt={prompt} data-page-title={pageTitle} style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.4rem",
  padding: "0.4rem 0.85rem",
  borderRadius: "8px",
  border: "none",
  cursor: "pointer",
  fontSize: "0.8rem",
  fontWeight: 600,
  color: "#ffffff",
  background: "#5E05C4"
}}>
          <Icon icon="copy" iconType="regular" size={13} color="#ffffff" />
          Copy prompt
        </button>
        <a href="#" onClick={vibePromptOpen} data-target="cursor" data-prompt={prompt} data-page-title={pageTitle} className="vibe-prompt__link" style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.4rem",
  padding: "0.4rem 0.75rem",
  borderRadius: "8px",
  border: "1px solid var(--vanta-border, rgba(120, 120, 130, 0.25))",
  fontSize: "0.8rem",
  fontWeight: 500,
  textDecoration: "none",
  color: "inherit",
  background: "transparent"
}}>
          <Icon icon="arrow-up-right-from-square" iconType="regular" size={11} color="#5E05C4" /><span>Cursor</span>
        </a>
        <a href="#" onClick={vibePromptOpen} data-target="claude" data-prompt={prompt} data-page-title={pageTitle} className="vibe-prompt__link" style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.4rem",
  padding: "0.4rem 0.75rem",
  borderRadius: "8px",
  border: "1px solid var(--vanta-border, rgba(120, 120, 130, 0.25))",
  fontSize: "0.8rem",
  fontWeight: 500,
  textDecoration: "none",
  color: "inherit",
  background: "transparent"
}}>
          <Icon icon="arrow-up-right-from-square" iconType="regular" size={11} color="#5E05C4" /><span>Claude</span>
        </a>
        <a href="#" onClick={vibePromptOpen} data-target="chatgpt" data-prompt={prompt} data-page-title={pageTitle} className="vibe-prompt__link" style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.4rem",
  padding: "0.4rem 0.75rem",
  borderRadius: "8px",
  border: "1px solid var(--vanta-border, rgba(120, 120, 130, 0.25))",
  fontSize: "0.8rem",
  fontWeight: 500,
  textDecoration: "none",
  color: "inherit",
  background: "transparent"
}}>
          <Icon icon="arrow-up-right-from-square" iconType="regular" size={11} color="#5E05C4" /><span>ChatGPT</span>
        </a>
      </div>
    </div>
  </div>;

export const VibePrompts = ({children}) => <section className="vibe-prompts" style={{
  marginTop: "2.5rem",
  marginBottom: "2rem",
  padding: "1.5rem 1.5rem 1.25rem",
  borderRadius: "16px",
  border: "1px solid color-mix(in srgb, #5E05C4 22%, transparent)",
  background: "linear-gradient(180deg, color-mix(in srgb, #5E05C4 6%, transparent) 0%, transparent 100%)"
}}>
    <div style={{
  display: "flex",
  alignItems: "center",
  gap: "0.65rem",
  marginBottom: "0.25rem"
}}>
      <div style={{
  display: "inline-flex",
  alignItems: "center",
  justifyContent: "center",
  width: "32px",
  height: "32px",
  borderRadius: "9px",
  background: "color-mix(in srgb, #5E05C4 14%, transparent)"
}}>
        <Icon icon="wand-magic-sparkles" iconType="regular" size={16} color="#5E05C4" />
      </div>
      <div>
        <div className="text-gray-900 dark:text-white" style={{
  fontFamily: "Reckless, Georgia, serif",
  fontSize: "1.25rem",
  fontWeight: 500,
  lineHeight: 1.15
}}>
          Let AI do this for you
        </div>
        <div className="text-gray-600 dark:text-gray-300" style={{
  fontSize: "0.85rem",
  lineHeight: 1.45,
  marginTop: "0.15rem"
}}>
          Copy the prompt to run this live via the <a href="/docs/vanta-mcp" style={{
  color: "#5E05C4"
}}> MCP server</a> or have AI generate a runnable script.
        </div>
      </div>
    </div>
    {children}
  </section>;

export const UPLOAD_DOC_BUILD = "You are writing a production-quality Node.js 18+ script that runs on a recurring schedule (cron / GitHub Actions / Cloud Scheduler) to keep Vanta documents fresh. The script picks up new evidence files from a local directory, uploads them, and submits the documents. Use the Manage Vanta API (base URL https://api.vanta.com).\n\nSteps:\n\n1. Read VANTA_CLIENT_ID, VANTA_CLIENT_SECRET, and EVIDENCE_DIR from env. Files in EVIDENCE_DIR are named \"<documentId>.<ext>\" where ext is one of pdf, docx, jpg, png, xlsx. Also accept STATE_PATH (default ./.vanta-uploads-state.json), DRY_RUN (default \"false\"), and DESCRIPTION_TEMPLATE (default \"Automated evidence upload — {date}\"). Fail fast on stderr if any required var is missing.\n2. Mint a Vanta API token via POST https://api.vanta.com/oauth/token with Content-Type: application/json and a JSON body containing client_id, client_secret, grant_type=\"client_credentials\", and scope=\"vanta-api.all:read vanta-api.all:write vanta-api.documents:upload\". The scope field is required — omitting it returns invalid_scope. Read access_token from the response. Re-mint at the start of each run (tokens expire after one hour).\n3. Page through GET https://api.vanta.com/v1/documents?pageSize=100 using the pageCursor request param, reading results.pageInfo.endCursor and hasNextPage until hasNextPage is false. Collect every document where uploadStatus is \"Needs document\" or \"Needs update\".\n4. Scan EVIDENCE_DIR for files. Build a map of documentId → local file path by stripping the extension. Skip files whose stem doesn't match any returned document.\n5. Load STATE_PATH if present — { \"lastSubmittedAt\": { \"<documentId>\": \"<isoTimestamp>\" } } — and skip any document whose local file mtime is older than the recorded lastSubmittedAt (already uploaded that revision).\n6. Print the plan: documentId | title | category | uploadStatus | localFile | action (\"upload+submit\" / \"skip-no-file\" / \"skip-already-uploaded\").\n7. If DRY_RUN === \"true\", print a JSON summary line and exit 0 without writing.\n8. For each document with action=\"upload+submit\":\n   a. POST https://api.vanta.com/v1/documents/<documentId>/uploads as multipart/form-data with fields file (binary file contents and filename), description (rendered from DESCRIPTION_TEMPLATE; replace {date} with new Date().toISOString().slice(0,10)). Do NOT set Content-Type manually — let the multipart boundary be added by the HTTP client.\n   b. POST https://api.vanta.com/v1/documents/<documentId>/submit with no body. A 200 response means the document flipped to OK.\n   c. On per-document success, record state[documentId] = file mtime ISO timestamp; write STATE_PATH atomically (temp file + rename) after every successful submit so a crash mid-batch doesn't lose progress.\n9. Issue uploads sequentially (no parallelism) to keep ordering deterministic and stay under rate limits.\n10. End with a single JSON summary line on stdout: {\"total\": N, \"uploaded\": U, \"skippedNoFile\": F, \"skippedAlreadyUploaded\": A, \"errors\": E, \"byCategory\": {...}}.\n\nSample API response item from GET /v1/documents (use these field names exactly):\n{ \"id\": \"access-requests\", \"ownerId\": \"1\", \"category\": \"Account setup\", \"description\": \"Provide two examples of a recent access request and approval\", \"isSensitive\": false, \"title\": \"Access request ticket and history\", \"uploadStatus\": \"Needs document\", \"uploadStatusDate\": \"2024-03-17T00:00:00.000Z\", \"url\": \"https://example.com\" }\n\nSample API response item from POST /v1/documents/<documentId>/uploads:\n{ \"id\": \"66a935ff0dfddd9e7c568558\", \"creationDate\": \"2024-07-30T18:50:39.419Z\", \"description\": \"Automated evidence upload — 2024-07-30\", \"fileName\": \"access-requests.pdf\", \"title\": \"Manual Evidence\", \"mimeType\": \"application/pdf\", \"url\": \"https://app.vanta.com/...\", \"uploadedByUserId\": null }\n\nMissing-field handling: if category, framework, or any optional field is missing on a document, print \"—\" for that field. Never throw on a missing optional field. If a local file is unreadable, log the path and continue with the next document.\n\nError handling:\n- 401: re-mint once and retry the request.\n- 404 on uploads or submit: log documentId + response body and continue.\n- 415 (Content-Type): the request must be multipart/form-data — confirm Content-Type is NOT set manually. Log and continue.\n- 403: scope is missing vanta-api.documents:upload. Log a clear message and exit non-zero.\n- 4xx on submit (no draft uploads): log documentId, retry once after re-running step 8a; otherwise mark ERROR and continue.\n- 429: respect Retry-After header if present, otherwise sleep 5s; retry up to 3 times.\n- 5xx: retry with 2s exponential back-off, max 3 attempts.\n\nDo not:\n- Add external dependencies — use built-in fetch, fs, and path only (FormData and Blob are built-in in Node 18+).\n- Hard-code document ids, titles, frameworks, or categories — every customer's document set is different. Discover everything via GET /v1/documents.\n- Re-upload the same file revision (use STATE_PATH to track last-submitted mtime).\n- Apply changes when DRY_RUN is \"true\".\n- Cache the access token across runs.\n- Silently swallow errors.\n\nDone when: a follow-up run with no new files in EVIDENCE_DIR reports 0 uploads (idempotent). Exit 0 if errors === 0; otherwise exit 1.\n\nScope required: vanta-api.all:read vanta-api.all:write vanta-api.documents:upload.";

This guide attaches an evidence file to a Vanta document with `POST /v1/documents/{documentId}/uploads` and submits it for review with `POST /v1/documents/{documentId}/submit` on the Manage Vanta API. The build prompt below generates a scheduled job that watches a local directory of evidence files named `<documentId>.<ext>`, uploads each matching file as multipart/form-data, and flips the document to `OK` — idempotent across runs using local file mtime.

<VibePrompts>
  <BuildPrompt prompt={UPLOAD_DOC_BUILD} />
</VibePrompts>

## Before you begin

This guide is for Vanta admins managing data inside their own Vanta account.

You'll need:

* A Manage Vanta [API token](/docs/quickstart/manage-vanta).
* The token must have scopes `vanta-api.all:read`, `vanta-api.all:write`, `vanta-api.documents:upload`.
* The evidence file you want to upload (PDF, image, etc.) saved locally.

<Info>
  Re-uploading evidence on a recurring schedule (e.g. quarterly access reviews)? Wire this flow into a cron job or CI step using the same three calls below.
</Info>

<Steps>
  <Step title="Find the document">
    **Your terminal** — call [`GET /v1/documents`](/reference/manage-vanta/overview) and pick the document you want to attach a file to.

    ```bash Terminal theme={"system"}
    curl 'https://api.vanta.com/v1/documents?pageSize=100&frameworkMatchesAny=soc2' \
      -H 'Accept: application/json' \
      -H 'Authorization: Bearer YOUR_TOKEN'
    ```

    Response

    ```json theme={"system"}
    {
      "results": {
        "data": [
          {
            "id": "access-requests",
            "title": "Access request ticket and history",
            "category": "Account setup",
            "overallStatus": "Needs document",
            "url": "https://app.vanta.com/documents/access-requests"
          }
        ],
        "pageInfo": { "hasNextPage": true, "endCursor": "..." }
      }
    }
    ```

    Copy the `id` field — you'll send it as the path parameter in the next step.

    <AccordionGroup>
      <Accordion title="Got a 401?">
        Token is expired (one-hour lifetime), missing, or lacks `vanta-api.all:read`. Mint a fresh one — see [Authentication → Token expiration](/docs/concepts/authentication#token-expiration).
      </Accordion>

      <Accordion title="Can't find your document?">
        Filter the response client-side by `title` or `category`, paginate with `pageCursor` if `hasNextPage` is `true`, or copy the ID directly from the [Documents page](https://app.vanta.com/documents) URL.
      </Accordion>

      <Accordion title="Need only documents missing evidence?">
        Add `statusMatchesAny=Needs%20document` (or `Needs%20update`) to narrow the response to documents that still require an upload.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Upload the file">
    **Your terminal** — `POST /v1/documents/{documentId}/uploads` as `multipart/form-data` with the file in the `file` field.

    <CodeGroup>
      ```javascript Node.js theme={"system"}
      import fs from "node:fs";

      const DOCUMENT_ID = "YOUR_DOCUMENT_ID"; // id from Step 1
      const FILE_PATH = "./access-requests.pdf";

      const form = new FormData();
      form.append(
        "file",
        new Blob([fs.readFileSync(FILE_PATH)], { type: "application/pdf" }),
        "access-requests.pdf",
      );
      form.append("description", "Q3 access review evidence");

      const res = await fetch(
        `https://api.vanta.com/v1/documents/${DOCUMENT_ID}/uploads`,
        {
          method: "POST",
          headers: { Authorization: "Bearer YOUR_TOKEN" },
          body: form,
        },
      );
      console.log(await res.json());
      ```

      ```python Python theme={"system"}
      import requests

      DOCUMENT_ID = "YOUR_DOCUMENT_ID"  # id from Step 1
      FILE_PATH = "./access-requests.pdf"

      with open(FILE_PATH, "rb") as f:
          r = requests.post(
              f"https://api.vanta.com/v1/documents/{DOCUMENT_ID}/uploads",
              headers={"Authorization": "Bearer YOUR_TOKEN"},
              files={"file": ("access-requests.pdf", f, "application/pdf")},
              data={"description": "Q3 access review evidence"},
          )
      r.raise_for_status()
      print(r.json())
      ```

      ```bash Terminal theme={"system"}
      DOCUMENT_ID="YOUR_DOCUMENT_ID"  # id from Step 1

      curl -X POST "https://api.vanta.com/v1/documents/$DOCUMENT_ID/uploads" \
        -H 'Accept: application/json' \
        -H 'Authorization: Bearer YOUR_TOKEN' \
        -F 'file=@"./access-requests.pdf"' \
        -F 'description="Q3 access review evidence"'
      ```
    </CodeGroup>

    Expected response (`200`) — the uploaded file metadata:

    ```json theme={"system"}
    {
      "id": "66a935ff0dfddd9e7c568558",
      "creationDate": "2024-07-30T18:50:39.419Z",
      "updatedDate": "2024-07-30T18:50:39.419Z",
      "deletionDate": null,
      "description": "Q3 access review evidence",
      "effectiveDate": "2024-07-30T18:50:39.177Z",
      "fileName": "access-requests.pdf",
      "title": "Manual Evidence",
      "mimeType": "application/pdf",
      "url": "https://app.vanta.com/acme/doc/Manual%20Evidence-5l14b5mljzt50h3uoo82sk",
      "uploadedByUserId": null
    }
    ```

    <Warning>
      The upload lands in a **draft** state. It won't satisfy the document until you submit it in Step 3.
    </Warning>

    <AccordionGroup>
      <Accordion title="Got a 404?">
        The document ID is wrong. Re-run Step 1 and copy `id` exactly — IDs may be human-readable (e.g. `access-requests`) or a 24-character ObjectId (e.g. `6596e2a8f62c73fb64960581`).
      </Accordion>

      <Accordion title="Got a 415 or `Content-Type` error?">
        The request must be `multipart/form-data` — don't set `Content-Type` manually, let your HTTP client add the boundary. Pass the file with `-F` (curl), `files=` (requests), or a `FormData` body (fetch).
      </Accordion>

      <Accordion title="Got a 403?">
        Your token is missing `vanta-api.documents:upload`. This is a separate scope from `vanta-api.all:write` — uploads require it explicitly. Mint a new token that includes all three scopes (`vanta-api.all:read`, `vanta-api.all:write`, `vanta-api.documents:upload`).
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Submit the document">
    **Your terminal** — `POST /v1/documents/{documentId}/submit` to move the document from `draft` to `OK`. Until you do this, auditors won't see the new evidence.

    <CodeGroup>
      ```javascript Node.js theme={"system"}
      const DOCUMENT_ID = "YOUR_DOCUMENT_ID"; // id from Step 1

      const res = await fetch(
        `https://api.vanta.com/v1/documents/${DOCUMENT_ID}/submit`,
        {
          method: "POST",
          headers: { Authorization: "Bearer YOUR_TOKEN" },
        },
      );
      console.log(res.status);
      ```

      ```python Python theme={"system"}
      import requests

      DOCUMENT_ID = "YOUR_DOCUMENT_ID"  # id from Step 1

      r = requests.post(
          f"https://api.vanta.com/v1/documents/{DOCUMENT_ID}/submit",
          headers={"Authorization": "Bearer YOUR_TOKEN"},
      )
      r.raise_for_status()
      print(r.status_code)
      ```

      ```bash Terminal theme={"system"}
      DOCUMENT_ID="YOUR_DOCUMENT_ID"  # id from Step 1

      curl -X POST "https://api.vanta.com/v1/documents/$DOCUMENT_ID/submit" \
        -H 'Accept: application/json' \
        -H 'Authorization: Bearer YOUR_TOKEN'
      ```
    </CodeGroup>

    A `200` response means the document's `overallStatus` has flipped to `OK` and the upload is visible to auditors.

    <AccordionGroup>
      <Accordion title="Got a 404?">
        Same root cause as Step 2 — re-check the `documentId` you copied from Step 1.
      </Accordion>

      <Accordion title="Got a 4xx on submit?">
        The document has no draft uploads to submit. Re-run Step 2, then submit. You can also confirm the upload exists with `GET /v1/documents/{documentId}/uploads`.
      </Accordion>
    </AccordionGroup>
  </Step>
</Steps>

<Info>
  **Replacing stale evidence?** Repeat Step 2 with the new file, then Step 3 — the latest submitted upload becomes the canonical evidence for the document.
</Info>

## Congratulations

You've attached an evidence file to a Vanta document and submitted it for review. The document's status is now `OK`, the upload is captured in your audit log, and auditors can pull the file directly from Vanta.

## Next steps

<CardGroup cols={2}>
  <Card title="Assign a control owner" icon="user-check" href="/docs/guides/add-an-owner-to-a-control">
    Make a specific user accountable for the controls this document supports.
  </Card>

  <Card title="Subscribe to webhooks" icon="bolt" href="/docs/webhooks">
    React in real time when document status or uploads change.
  </Card>

  <Card title="Try it in Postman" icon="paper-plane" href="/docs/postman-setup">
    Import the collection and run `uploads` + `submit` against a sandbox in seconds.
  </Card>

  <Card title="Manage Vanta API reference" icon="book" href="/reference/manage-vanta/overview">
    Browse every Manage Vanta endpoint — controls, tests, documents, people.
  </Card>
</CardGroup>
