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

# Set custom fields on vendors

> Attach organization-specific metadata — contract dates, internal owners, risk tiers, anything you track — to vendors when creating or updating them, 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 VENDOR_CUSTOM_FIELDS_BUILD = "You are writing a production-quality Node.js 18+ script that runs on a recurring schedule (cron / GitHub Actions / Cloud Scheduler) to keep vendor custom fields in Vanta in sync with a CSV exported from a procurement / GRC system. Use the Manage Vanta API (base URL https://api.vanta.com).\n\nSteps:\n\n1. Read VANTA_CLIENT_ID, VANTA_CLIENT_SECRET, and CSV_PATH from env. Also accept STATE_PATH (default ./.vanta-vendor-fields-state.json) and DRY_RUN (default \"false\"). 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\". 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. Parse CSV_PATH using a lightweight built-in parser that handles quoted fields and commas inside quotes. The first column must be websiteUrl; every other column header is the literal label of a custom field already defined in Vanta vendor settings.\n4. Page through GET https://api.vanta.com/v1/vendors?pageSize=100 using the pageCursor request param (read results.pageInfo.endCursor and hasNextPage until hasNextPage is false). Build a Map keyed by websiteUrl normalized to lowercase + trimmed. Each value is the full vendor object.\n5. For each CSV row, look up the vendor by normalized websiteUrl. Classify as missing-vendor when no match. For each non-websiteUrl column, coerce the CSV cell to the field's expected JSON shape: empty cell -> null; numeric cell -> JSON number; \"true\"/\"false\" (case-insensitive) -> boolean; ISO 8601 date string -> string; everything else -> string.\n6. Diff each row's coerced values against the vendor's existing customFields[] (label match is exact, case-sensitive). Build a `changes` array of { label, value } only for entries that differ from the current value. If `changes` is empty, classify the row as skipped.\n7. Load STATE_PATH if present — { \"lastSyncedHash\": { \"<vendorId>\": \"<sha-of-changes>\" } } — and skip rows whose computed hash already matches (idempotent re-runs).\n8. Print the plan: vendorId | name | websiteUrl | fieldsToUpdate (count) | classification (updated / skipped / missing-vendor).\n9. If DRY_RUN === \"true\", print a JSON summary and exit 0 without writing.\n10. For each row classified as updated, PATCH https://api.vanta.com/v1/vendors/{vendorId} with body { \"customFields\": <changes> }. Send only the changed fields — omitted labels keep their current value. Issue requests sequentially to keep ordering deterministic and stay under rate limits.\n11. After every successful PATCH, record state.lastSyncedHash[vendorId] = sha of changes; write STATE_PATH atomically (temp file + rename). Log id, name, websiteUrl, updatedFields[], status (SUCCESS / ERROR), and any error message.\n12. End with a single JSON summary line on stdout: {\"total\": N, \"updated\": U, \"skipped\": S, \"missingVendors\": M, \"errors\": E}.\n\nSample API response item from GET /v1/vendors (use these field names exactly):\n{ \"id\": \"a2f7e1b9d0c3f4e5a6c7b8d8\", \"name\": \"Vanta\", \"websiteUrl\": \"https://www.vanta.com/\", \"category\": { \"displayName\": \"cloudMonitoring\" }, \"inherentRiskLevel\": \"HIGH\", \"residualRiskLevel\": \"MEDIUM\", \"contractStartDate\": \"2024-02-01T00:00:00.000Z\", \"contractRenewalDate\": \"2025-02-01T00:00:00.000Z\", \"customFields\": [{ \"label\": \"contractEndDate\", \"value\": \"2026-01-01T00:00:00.000Z\" }, { \"label\": \"internalRiskTier\", \"value\": \"Tier 2\" }] }\n\nSample API response from PATCH /v1/vendors/{vendorId}: the full vendor object with the updated customFields[] populated.\n\nMissing-field handling: if customFields is null on the vendor, treat as []. If a CSV column header doesn't match any defined custom field in Vanta, the API will return 400 — log that label and skip the row. Never throw on a missing optional field.\n\nError handling:\n- 401: re-mint once and retry the request.\n- 400 saying the custom field doesn't exist: log the label, mark the row as ERROR, continue. Field labels must be defined in the Vanta UI — the API only fills them.\n- 400 saying the value type is wrong: log the field, expected vs actual type, and the offending value; mark ERROR and continue.\n- 404 on PATCH: vendor was deleted between the list and patch. Log 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; then log and exit non-zero only if every PATCH failed.\n\nDo not:\n- Add external dependencies — use built-in fetch, fs, path, and crypto only. The CSV parser is a small built-in helper, not an npm package.\n- Hard-code any vendor id, websiteUrl, custom field label, or value — every customer's vendor list and field schema is different. Always read CSV_PATH and discover vendors via /v1/vendors.\n- Send `category` in the same PATCH as `customFields` — combining them wipes category-derived risk attributes before applying custom values. customFields-only PATCH is safe.\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 an unchanged CSV reports 0 updates (idempotent). Exit 0 if errors === 0; otherwise exit 1.\n\nScope required: vanta-api.all:read vanta-api.all:write.";

This guide writes organization-specific metadata — contract dates, internal owners, risk tiers, ticket links — to vendors using `PATCH /v1/vendors/{vendorId}` with a `customFields` payload on the Manage Vanta API. Custom field labels must be defined in the Vanta vendor settings UI first; the API only fills in their values. The build prompt below generates a scheduled job that syncs a CSV export from a procurement or GRC system into Vanta, only PATCHing vendors whose fields actually changed.

<VibePrompts>
  <BuildPrompt prompt={VENDOR_CUSTOM_FIELDS_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`.
* The custom fields you want to set must be defined ahead of time in the Vanta UI (the API can only **fill** custom fields, not create them).

Custom fields let you attach organization-specific metadata to vendors — contract end dates, internal owners, risk ratings, or anything else you track. They're defined in the [Vendor settings page](https://app.vanta.com/vendors) by name and type, then set or updated via the API.

<Info>
  Importing a backlog of vendors from your procurement system? Define every custom field you'll send first in the [Vendor settings page](https://app.vanta.com/vendors), then run Step 1 in a loop. Field names sent in the API must match the Vanta-side `label` exactly — including case, and values must match the field's type:

  * `String` → JSON string: `"value": "Tier 1"`
  * `Number` → JSON number: `"value": 10`
  * `Date` → ISO 8601 string: `"value": "2026-01-01T00:00:00.000Z"`
  * `Boolean` → JSON bool: `"value": true`
</Info>

<Steps>
  <Step title="Set custom fields when creating a vendor">
    **Your terminal** — `POST /v1/vendors` with a `customFields` array. Each entry has `label` (matches the Vanta-side field name) and `value` (matches the field's type).

    <CodeGroup>
      ```javascript Node.js theme={"system"}
      const res = await fetch("https://api.vanta.com/v1/vendors", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: "Bearer YOUR_TOKEN",
        },
        body: JSON.stringify({
          name: "Acme Corp",
          websiteUrl: "https://acmecorp.com",
          category: "cloudStorage",
          customFields: [
            { label: "contractEndDate", value: "2026-01-01T00:00:00.000Z" },
            { label: "internalRiskTier", value: "Tier 2" },
          ],
        }),
      });
      const vendor = await res.json();
      console.log(vendor.id);
      ```

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

      r = requests.post(
          "https://api.vanta.com/v1/vendors",
          headers={"Authorization": "Bearer YOUR_TOKEN"},
          json={
              "name": "Acme Corp",
              "websiteUrl": "https://acmecorp.com",
              "category": "cloudStorage",
              "customFields": [
                  {"label": "contractEndDate", "value": "2026-01-01T00:00:00.000Z"},
                  {"label": "internalRiskTier", "value": "Tier 2"},
              ],
          },
      )
      r.raise_for_status()
      print(r.json()["id"])
      ```

      ```bash Terminal theme={"system"}
      curl -X POST 'https://api.vanta.com/v1/vendors' \
        -H 'Content-Type: application/json' \
        -H 'Accept: application/json' \
        -H 'Authorization: Bearer YOUR_TOKEN' \
        -d '{
          "name": "Acme Corp",
          "websiteUrl": "https://acmecorp.com",
          "category": "cloudStorage",
          "customFields": [
            { "label": "contractEndDate", "value": "2026-01-01T00:00:00.000Z" },
            { "label": "internalRiskTier", "value": "Tier 2" }
          ]
        }'
      ```
    </CodeGroup>

    Expected response (`200`) — the full vendor object, including the values you just set:

    ```json theme={"system"}
    {
      "id": "a2f7e1b9d0c3f4e5a6c7b8d8",
      "name": "Acme Corp",
      "category": { "displayName": "cloudStorage" },
      "customFields": [
        { "label": "contractEndDate", "value": "2026-01-01T00:00:00.000Z" },
        { "label": "internalRiskTier", "value": "Tier 2" }
      ]
    }
    ```

    <AccordionGroup>
      <Accordion title="Got a 400 saying the custom field doesn't exist?">
        The `label` doesn't match a defined custom field. Check the [Vendor settings page](https://app.vanta.com/vendors) — labels are case-sensitive and must match exactly.
      </Accordion>

      <Accordion title="Got a 400 saying the value type is wrong?">
        Your JSON value doesn't match the custom field's type. A `Date` field needs an ISO 8601 string; a `Number` needs a JSON number, not a string; a `Boolean` needs `true`/`false`, not `"true"`/`"false"`.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Update custom fields on an existing vendor">
    **Your terminal** — `PATCH /v1/vendors/{vendorId}` with the same `customFields` shape. Only the fields you send are changed; omitted fields keep their current values.

    <CodeGroup>
      ```javascript Node.js theme={"system"}
      const VENDOR_ID = "YOUR_VENDOR_ID";

      const res = await fetch(`https://api.vanta.com/v1/vendors/${VENDOR_ID}`, {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
          Authorization: "Bearer YOUR_TOKEN",
        },
        body: JSON.stringify({
          customFields: [
            { label: "contractEndDate", value: "2027-01-01T00:00:00.000Z" },
          ],
        }),
      });
      console.log(await res.json());
      ```

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

      VENDOR_ID = "YOUR_VENDOR_ID"

      r = requests.patch(
          f"https://api.vanta.com/v1/vendors/{VENDOR_ID}",
          headers={"Authorization": "Bearer YOUR_TOKEN"},
          json={
              "customFields": [
                  {"label": "contractEndDate", "value": "2027-01-01T00:00:00.000Z"},
              ]
          },
      )
      r.raise_for_status()
      print(r.json())
      ```

      ```bash Terminal theme={"system"}
      VENDOR_ID="YOUR_VENDOR_ID"

      curl -X PATCH "https://api.vanta.com/v1/vendors/$VENDOR_ID" \
        -H 'Content-Type: application/json' \
        -H 'Accept: application/json' \
        -H 'Authorization: Bearer YOUR_TOKEN' \
        -d '{
          "customFields": [
            { "label": "contractEndDate", "value": "2027-01-01T00:00:00.000Z" }
          ]
        }'
      ```
    </CodeGroup>

    A `200` response returns the updated vendor with its new `customFields`.

    <Warning>
      Sending `customFields` with `category` in the same call wipes any category-derived risk attributes before applying your custom values — see the [Create a vendor guide](/docs/guides/create-vendors-and-attach-documentation) for the ordering rules.
    </Warning>

    <AccordionGroup>
      <Accordion title="Got a 404?">
        The `vendorId` is wrong. Re-run [`GET /v1/vendors`](/reference/manage-vanta/overview) and copy the `id` exactly, or grab it from the URL on the [Vendor page](https://app.vanta.com/vendors).
      </Accordion>

      <Accordion title="Need to clear a custom field?">
        Send the field with `value: null`. The label still has to match a defined field.
      </Accordion>
    </AccordionGroup>
  </Step>
</Steps>

<Info>
  **Bulk-syncing from a procurement system?** List vendors with `GET /v1/vendors`, match each one to your source-of-truth record by `name` or `websiteUrl`, then run Step 2 per vendor with the latest custom-field values. Tokens expire hourly — re-mint at the top of each run.
</Info>

## Congratulations

You've populated organization-specific metadata on your vendors. Custom field values are visible on each vendor's profile, queryable via the API, and included in vendor exports for auditors.

## Next steps

<CardGroup cols={2}>
  <Card title="Create a vendor and attach evidence" icon="building" href="/docs/guides/create-vendors-and-attach-documentation">
    Set up a new vendor end-to-end with metadata, custom fields, and a SOC 2 report.
  </Card>

  <Card title="Assign a control owner" icon="user-check" href="/docs/guides/add-an-owner-to-a-control">
    Make a specific user accountable for vendor-management controls.
  </Card>

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

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