> ## 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.

# Offboard personnel

> Complete the offboarding workflow for ex-employees — deactivating unmonitored accounts and recording an acknowledger — 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 OFFBOARD_BUILD = "You are writing a production-quality Node.js 18+ script that runs on a daily schedule (cron / GitHub Actions / Cloud Scheduler) to drain the offboarding queue for FORMER employees in Vanta. Use the Manage Vanta API (base URL https://api.vanta.com).\n\nSteps:\n\n1. Read VANTA_CLIENT_ID, VANTA_CLIENT_SECRET, and ACKNOWLEDGER_EMAIL from env. Also accept STATE_PATH (default ./.vanta-offboard-state.json) and DRY_RUN (default \"false\"). Fail fast on stderr if any required var is missing — do not hard-code an email or user id in the script.\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\". 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. Resolve ACKNOWLEDGER_EMAIL to a user id by paging GET https://api.vanta.com/v1/people?employmentStatusMatchesAny=CURRENT&pageSize=100 with the pageCursor request param (read results.pageInfo.endCursor and hasNextPage until hasNextPage is false). Match emailAddress case-insensitively and capture the id as ACKNOWLEDGER_ID. Exit non-zero if not found, or if employment.status is not CURRENT (offboarding fails on a FORMER acknowledger).\n4. Page through GET https://api.vanta.com/v1/people?employmentStatusMatchesAny=FORMER&pageSize=100 using pageCursor pagination. For each person, classify as:\n   - already-complete — tasksSummary.status === \"OFFBOARDING_COMPLETE\"\n   - blocked — tasksSummary.details has any entry with status !== \"COMPLETE\" for completeCustomOffboardingTasks, or any monitored-account-related tasks not yet completed\n   - eligible — everyone else\n5. Load STATE_PATH if present — { \"alreadyOffboarded\": [personId, ...] } — and remove those from \"eligible\".\n6. Print the plan: personId | displayName | emailAddress | endDate | classification | blockers (if any).\n7. If DRY_RUN === \"true\", print a JSON summary line and exit 0 without writing.\n8. Batch eligible people into chunks of 1000 and POST each chunk to https://api.vanta.com/v1/people/offboard with body { \"updates\": [{ \"id\": \"<personId>\", \"acknowledgerId\": \"<ACKNOWLEDGER_ID>\" }, ...] }. Issue chunks sequentially.\n9. Parse the per-entry results array. For every entry where status === \"SUCCESS\" append the id to alreadyOffboarded; write STATE_PATH atomically (temp file + rename) after every batch so a crash doesn't lose progress. For every \"ERROR\" entry, log id, displayName, and the message field — do not throw.\n10. Print one line per processed person: emailAddress | displayName | endDate | classification | status (SUCCESS / ERROR / BLOCKED / SKIPPED) | message (if any).\n11. End with a single JSON summary line on stdout: {\"total\": N, \"offboarded\": O, \"blocked\": B, \"alreadyComplete\": C, \"errors\": E}.\n\nSample API response item from GET /v1/people (use these field names exactly):\n{ \"id\": \"635c369a274dff2743f29160\", \"emailAddress\": \"former-employee@example.com\", \"name\": { \"display\": \"Adrian Test\", \"first\": \"Adrian\", \"last\": \"Test\" }, \"employment\": { \"status\": \"FORMER\", \"jobTitle\": \"Engineer\", \"startDate\": \"2022-01-01T00:00:00.000Z\", \"endDate\": \"2024-09-21T00:00:00.000Z\" }, \"tasksSummary\": { \"status\": \"OVERDUE\", \"dueDate\": \"2024-09-28T00:00:00.000Z\", \"details\": { \"completeCustomOffboardingTasks\": { \"status\": \"OVERDUE\" } } } }\n\nSample API response from POST /v1/people/offboard:\n{ \"results\": [{ \"id\": \"635c369a274dff2743f29160\", \"status\": \"SUCCESS\" }, { \"id\": \"65a0caf8b4c7501f891e2183\", \"status\": \"ERROR\", \"message\": \"Custom offboarding task incomplete\" }] }\n\nMissing-field handling: if tasksSummary is null treat as \"blocked\" with reason \"no tasks summary\"; if endDate is null print \"—\". Never throw on a missing optional field.\n\nError handling:\n- 401: re-mint once and retry the request.\n- 400: log the offending request body and exit non-zero.\n- 403: scope missing vanta-api.all:write. Log and exit non-zero.\n- 429: respect Retry-After if present, otherwise sleep 5s; retry up to 3 times.\n- 5xx: retry with 2s exponential back-off, max 3 attempts; then log and exit non-zero.\n\nDo not:\n- Add external dependencies — use built-in fetch, fs, and path only.\n- Hard-code any person id, email, or acknowledger id — every customer's personnel list is different. Read ACKNOWLEDGER_EMAIL from env and discover everything else via the API.\n- Apply changes when DRY_RUN is \"true\".\n- Cache the access token across runs.\n- Send more than 1000 entries per /v1/people/offboard call.\n- Silently swallow errors.\n\nDone when: a follow-up run reports 0 eligible candidates (every FORMER employee is OFFBOARDING_COMPLETE or blocked). Exit 0 if errors === 0; otherwise exit 1.\n\nScope required: vanta-api.all:read vanta-api.all:write.";

This guide completes the offboarding workflow for departed employees using `POST /v1/people/offboard` on the Manage Vanta API — deactivating unmonitored accounts, recording an acknowledger, and marking each person's offboarding tasks complete. The build prompt below generates a daily job that drains the offboarding queue: it classifies every FORMER employee as already-complete, blocked, or eligible, then offboards the eligible ones in batches of up to 1000.

<VibePrompts>
  <BuildPrompt prompt={OFFBOARD_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` and `vanta-api.all:write`.
* For each person you're offboarding, all of the following must already be true:
  * Their employment `status` is `FORMER`.
  * All **monitored** accounts are deactivated (this is automatic when an integration reports the deactivation).
  * All **custom offboarding tasks** are complete.

`POST /v1/people/offboard` will mark **unmonitored** accounts deactivated and complete the offboarding for you. It will **not** auto-complete custom offboarding tasks — those have to be done in the Vanta UI before this call will succeed.

<Info>
  Wiring this into your HRIS? Run a daily job: query `GET /v1/people?employmentStatusMatchesAny=FORMER`, filter to people whose `tasksSummary.status` is not yet `OFFBOARDING_COMPLETE`, and call Step 3 in batches.
</Info>

<Steps>
  <Step title="Find people eligible for offboarding">
    **Your terminal** — call [`GET /v1/people`](/reference/manage-vanta/overview) and filter to `FORMER` employees so you only see candidates. Inspect each person's `tasksSummary.status` — anyone whose status is already `OFFBOARDING_COMPLETE` is done; anyone else with `status: FORMER` is a candidate.

    ```bash Terminal theme={"system"}
    curl 'https://api.vanta.com/v1/people?pageSize=50&employmentStatusMatchesAny=FORMER' \
      -H 'Accept: application/json' \
      -H 'Authorization: Bearer YOUR_TOKEN'
    ```

    Response (truncated)

    ```json theme={"system"}
    {
      "results": {
        "data": [
          {
            "id": "635c369a274dff2743f29160",
            "emailAddress": "former-employee@example.com",
            "name": { "display": "Adrian Test" },
            "employment": { "status": "FORMER", "endDate": "2024-09-21T00:00:00.000Z" },
            "tasksSummary": { "status": "OVERDUE" }
          }
        ]
      }
    }
    ```

    Copy the `id` of each candidate — you'll send them in Step 3.

    <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="`tasksSummary.status` is `OFFBOARDING_COMPLETE`?">
        That person is already offboarded. Skip them — calling `offboard` on them is a no-op.
      </Accordion>

      <Accordion title="Person still has custom offboarding tasks open?">
        `POST /v1/people/offboard` will fail until those are completed. Finish them on the [Personnel page](https://app.vanta.com/people/people) (or via your HRIS workflow) before calling Step 3.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Pick an acknowledger">
    **Your terminal** — every offboarding event records who acknowledged it (typically a security or HR admin). The `acknowledgerId` is **not** a Person `id` — it's the Vanta User Account ID of a `CURRENT` employee. Get it from either:

    * [`GET /v1/users`](/reference/manage-vanta/overview), or
    * the `userId` field on a person returned from [`GET /v1/people`](/reference/manage-vanta/overview).

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

    Copy the user `id` of the acknowledger you'll record on every entry. Most teams hard-code this to a specific compliance lead's user ID.

    <AccordionGroup>
      <Accordion title="Acknowledger must be `CURRENT`?">
        Yes. Offboarding fails if the `acknowledgerId` belongs to a `FORMER` employee.
      </Accordion>

      <Accordion title="Why isn't this the Person `id`?">
        `acknowledgerId` refers to the Vanta User Account ID, not the Person ID. Pull it from `GET /v1/users` or from the `userId` field on the corresponding person record.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Offboard the people">
    **Your terminal** — `POST /v1/people/offboard` with an `updates` array. Each entry needs the person `id` and the `acknowledgerId` you picked.

    <CodeGroup>
      ```javascript Node.js theme={"system"}
      const ACKNOWLEDGER_ID = "5df91759d463fd48218e9f15";

      const res = await fetch("https://api.vanta.com/v1/people/offboard", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: "Bearer YOUR_TOKEN",
        },
        body: JSON.stringify({
          updates: [
            { id: "635c369a274dff2743f29160", acknowledgerId: ACKNOWLEDGER_ID },
            { id: "65a0caf8b4c7501f891e2183", acknowledgerId: ACKNOWLEDGER_ID },
          ],
        }),
      });
      console.log(await res.json());
      ```

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

      ACKNOWLEDGER_ID = "5df91759d463fd48218e9f15"
      ids = ["635c369a274dff2743f29160", "65a0caf8b4c7501f891e2183"]

      r = requests.post(
          "https://api.vanta.com/v1/people/offboard",
          headers={"Authorization": "Bearer YOUR_TOKEN"},
          json={
              "updates": [
                  {"id": person_id, "acknowledgerId": ACKNOWLEDGER_ID}
                  for person_id in ids
              ]
          },
      )
      r.raise_for_status()
      print(r.json())
      ```

      ```bash Terminal theme={"system"}
      curl -X POST 'https://api.vanta.com/v1/people/offboard' \
        -H 'Content-Type: application/json' \
        -H 'Accept: application/json' \
        -H 'Authorization: Bearer YOUR_TOKEN' \
        -d '{
          "updates": [
            { "id": "635c369a274dff2743f29160", "acknowledgerId": "5df91759d463fd48218e9f15" },
            { "id": "65a0caf8b4c7501f891e2183", "acknowledgerId": "5df91759d463fd48218e9f15" }
          ]
        }'
      ```
    </CodeGroup>

    Expected response (`200`) — a per-person result so you can see which offboardings succeeded:

    ```json theme={"system"}
    {
      "results": [
        { "id": "635c369a274dff2743f29160", "status": "SUCCESS" },
        { "id": "65a0caf8b4c7501f891e2183", "status": "ERROR", "message": "Custom offboarding task incomplete" }
      ]
    }
    ```

    <Warning>
      Up to **1000** people per call. Successful entries are committed even if other entries return `ERROR` — fix and retry only the failures.
    </Warning>

    <AccordionGroup>
      <Accordion title="Got an `ERROR` saying tasks are incomplete?">
        The person still has open custom offboarding tasks or monitored accounts that haven't deactivated. Resolve those (typically by waiting for the next sync from your IDP/MDM) and retry just that `id`.
      </Accordion>

      <Accordion title="Got a 403?">
        Your token has `vanta-api.all:read` but not `vanta-api.all:write`. Mint a token with both scopes.
      </Accordion>

      <Accordion title="Need to confirm a single person was offboarded?">
        Re-call [`GET /v1/people/{personId}`](/reference/manage-vanta/overview). The `tasksSummary.status` flips to `OFFBOARDING_COMPLETE` when the offboarding is recorded.
      </Accordion>
    </AccordionGroup>
  </Step>
</Steps>

<Info>
  **Reassigning their controls first?** Run [Assign a control owner](/docs/guides/add-an-owner-to-a-control) for everything they own before this call so nothing ends up orphaned to a `FORMER` user.
</Info>

## Congratulations

You've completed the offboarding workflow for one or more ex-employees. Their unmonitored accounts are now marked deactivated, an acknowledger is recorded for the audit log, and their `tasksSummary.status` is `OFFBOARDING_COMPLETE`.

## Next steps

<CardGroup cols={2}>
  <Card title="Assign a control owner" icon="user-check" href="/docs/guides/add-an-owner-to-a-control">
    Reassign every control the offboarded person owned before they leave the system.
  </Card>

  <Card title="List overdue security tasks" icon="list-check" href="/docs/guides/list-users-with-overdue-security-tasks">
    Confirm the offboarded user no longer appears in your overdue-task report.
  </Card>

  <Card title="Try it in Postman" icon="paper-plane" href="/docs/postman-setup">
    Import the collection and run `offboard` 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>
