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

# Add owners to resources

> Use the Vanta API to update the owner and descriptions on your resources.

## 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`.
* At least one connected integration whose resources need owners or descriptions.

Inventory resources need an active owner and a text description to pass the [Inventory items have active owners](https://app.vanta.com/tests/inventory-list-owners) and [Inventory items have descriptions](https://app.vanta.com/tests/inventory-list-descriptions) tests. Owners must be `CURRENT` employees — Vanta will reject `FORMER` users.

<Info>
  Onboarding a new owner across many integrations? Run Step 3 once per `(integrationId, resourceKind)` pair. The bulk-update endpoint accepts up to 50 resources per call.
</Info>

<Steps>
  <Step title="Find the resources missing an owner">
    **Your terminal** — pick the `integrationId` and `resourceKind` you want to update. If you don't already know them, list connected integrations first with [`GET /v1/integrations`](/reference/manage-vanta/overview), then list resources filtered to `hasOwner=false`.

    ```bash Terminal theme={"system"}
    curl 'https://api.vanta.com/v1/integrations/snowflake/resource-kinds/SnowflakeDatabase/resources?hasOwner=false&isInScope=true&pageSize=50' \
      -H 'Accept: application/json' \
      -H 'Authorization: Bearer YOUR_TOKEN'
    ```

    Response

    ```json theme={"system"}
    {
      "results": {
        "data": [
          {
            "responseType": "Database",
            "resourceKind": "SnowflakeDatabase",
            "resourceId": "64dc848b6491f66d23c64df6",
            "connectionId": "64dbda1a6491f66d234bfd41",
            "displayName": "SNOWFLAKE",
            "owner": null,
            "inScope": true,
            "description": "Snowflake Database"
          }
        ],
        "pageInfo": { "hasNextPage": true, "endCursor": "..." }
      }
    }
    ```

    Copy each `resourceId` you want to update — 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="Don't know the integrationId or resourceKind?">
        Call [`GET /v1/integrations`](/reference/manage-vanta/overview) to list connected integrations, their `connectionIds`, and the `resourceKinds` they expose (e.g. `aws`, `gcp`, `snowflake`, `okta`).
      </Accordion>

      <Accordion title="Filtering by description too?">
        Add `hasDescription=false` to find resources missing a description. Some resource kinds (e.g. `SnowflakeDatabase`) auto-populate a description and won't show up in that filter.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Get a user to assign as owner">
    **Your terminal** — call [`GET /v1/people`](/reference/manage-vanta/overview) and filter to `CURRENT` employees so you don't pick someone who's offboarded.

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

    Response

    ```json theme={"system"}
    {
      "results": {
        "data": [
          {
            "id": "5fc82421a228f6b6f713547d",
            "emailAddress": "developers@vanta.com",
            "name": { "display": "Alejandro Ocampo" },
            "employment": { "status": "CURRENT", "jobTitle": "Security Engineer" }
          }
        ]
      }
    }
    ```

    Copy the `id`. You'll send it as `ownerId` in the next step.

    <AccordionGroup>
      <Accordion title="Can't find the user?">
        Filter client-side by `emailAddress` (most reliable), or paginate with `pageCursor` if `hasNextPage` is `true`. You can also copy a user's ID directly from their profile URL on the [People page](https://app.vanta.com/people/people).
      </Accordion>

      <Accordion title="They show as `FORMER`?">
        They've been offboarded and aren't eligible to own resources. Pick a `CURRENT` user instead.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Update owner and description in bulk">
    **Your terminal** — `PATCH /v1/integrations/{integrationId}/resource-kinds/{resourceKind}/resources` with an `updates` array. Each entry is keyed by the resource `id` and may set `ownerId`, `description`, and/or `inScope`.

    <CodeGroup>
      ```javascript Node.js theme={"system"}
      const INTEGRATION_ID = "snowflake";
      const RESOURCE_KIND = "SnowflakeDatabase";
      const OWNER_ID = "5fc82421a228f6b6f713547d";

      const res = await fetch(
        `https://api.vanta.com/v1/integrations/${INTEGRATION_ID}/resource-kinds/${RESOURCE_KIND}/resources`,
        {
          method: "PATCH",
          headers: {
            "Content-Type": "application/json",
            Authorization: "Bearer YOUR_TOKEN",
          },
          body: JSON.stringify({
            updates: [
              {
                id: "64dc848b6491f66d23c64df6",
                ownerId: OWNER_ID,
                description: "Primary Snowflake warehouse for analytics",
              },
              {
                id: "64dc848b6491f66d23c64e26",
                ownerId: OWNER_ID,
                description: "Sample dataset – read-only",
              },
            ],
          }),
        },
      );
      console.log(await res.json());
      ```

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

      INTEGRATION_ID = "snowflake"
      RESOURCE_KIND = "SnowflakeDatabase"
      OWNER_ID = "5fc82421a228f6b6f713547d"

      r = requests.patch(
          f"https://api.vanta.com/v1/integrations/{INTEGRATION_ID}/resource-kinds/{RESOURCE_KIND}/resources",
          headers={"Authorization": "Bearer YOUR_TOKEN"},
          json={
              "updates": [
                  {
                      "id": "64dc848b6491f66d23c64df6",
                      "ownerId": OWNER_ID,
                      "description": "Primary Snowflake warehouse for analytics",
                  },
                  {
                      "id": "64dc848b6491f66d23c64e26",
                      "ownerId": OWNER_ID,
                      "description": "Sample dataset – read-only",
                  },
              ]
          },
      )
      r.raise_for_status()
      print(r.json())
      ```

      ```bash Terminal theme={"system"}
      curl -X PATCH 'https://api.vanta.com/v1/integrations/snowflake/resource-kinds/SnowflakeDatabase/resources' \
        -H 'Content-Type: application/json' \
        -H 'Accept: application/json' \
        -H 'Authorization: Bearer YOUR_TOKEN' \
        -d '{
          "updates": [
            { "id": "64dc848b6491f66d23c64df6", "ownerId": "5fc82421a228f6b6f713547d", "description": "Primary Snowflake warehouse for analytics" },
            { "id": "64dc848b6491f66d23c64e26", "ownerId": "5fc82421a228f6b6f713547d", "description": "Sample dataset – read-only" }
          ]
        }'
      ```
    </CodeGroup>

    Expected response (`200`) — a per-resource result, so you can tell which updates failed:

    ```json theme={"system"}
    {
      "results": [
        { "id": "64dc848b6491f66d23c64df6", "status": "SUCCESS" },
        { "id": "64dc848b6491f66d23c64e26", "status": "SUCCESS" }
      ]
    }
    ```

    <Warning>
      The bulk endpoint accepts up to **50** resources per call. Page through Step 1 and call this endpoint in batches if you have more.
    </Warning>

    <AccordionGroup>
      <Accordion title="Got an `ERROR` in `results`?">
        The `message` field tells you which update failed and why — typically because `ownerId` is `FORMER`, the `id` doesn't belong to that `(integrationId, resourceKind)`, or the resource kind doesn't support `description`. Successful entries are still applied; only fix and retry the failures.
      </Accordion>

      <Accordion title="Got a 404?">
        The `integrationId` or `resourceKind` is wrong. They're case-sensitive — `snowflake` and `SnowflakeDatabase` are different fields. Re-run Step 1 to confirm the exact spellings.
      </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>
    </AccordionGroup>
  </Step>
</Steps>

<Info>
  **Single-resource update?** Use `PATCH /v1/integrations/{integrationId}/resource-kinds/{resourceKind}/resources/{resourceId}` with `{ "ownerId": "...", "description": "..." }` instead. It returns `204 No Content` on success.
</Info>

## Congratulations

You've assigned active owners and descriptions to your inventory resources. Those resources will now pass the **Inventory items have active owners** and **Inventory items have descriptions** tests, and the changes are captured in your audit log.

<VibePrompts>
  <BuildPrompt
    pageTitle="Add owners and descriptions to resources"
    prompt={`Build an idempotent script that backfills owners and descriptions on Vanta inventory resources via the Manage Vanta API. Reference the guide at the URL below for endpoint shapes, response envelopes, and edge cases.

INPUTS (read from env, fail loudly if missing):
- VANTA_TOKEN — Manage Vanta OAuth access token. Required scopes: vanta-api.all:read AND vanta-api.all:write. Tokens expire after one hour; do not cache across runs.
- INTEGRATION_ID — e.g. "snowflake". Case-sensitive.
- RESOURCE_KIND — e.g. "SnowflakeDatabase". Case-sensitive.
- DEFAULT_OWNER_EMAIL — email of a CURRENT employee. Script must verify the user resolves to status "CURRENT" before using them.
- DESCRIPTION_TEMPLATE — optional string with {displayName} and {resourceKind} tokens. Default: "{displayName} ({resourceKind})".
- DRY_RUN — boolean. When true, print the planned PATCH bodies and exit without writing.

BASE URL: https://api.vanta.com
HEADERS on every request: Authorization: Bearer \${VANTA_TOKEN}, Accept: application/json. Add Content-Type: application/json on PATCH.

STEP 1 — Resolve the owner.
GET /v1/people?employmentStatusMatchesAny=CURRENT&pageSize=100. Page using results.pageInfo.endCursor + &pageCursor=ENDCURSOR until results.pageInfo.hasNextPage is false. Find the user where emailAddress equals DEFAULT_OWNER_EMAIL (case-insensitive). Capture their id as OWNER_ID. If not found, or status != "CURRENT", exit 4 with a clear message.

STEP 2 — Collect resources missing an owner.
GET /v1/integrations/{INTEGRATION_ID}/resource-kinds/{RESOURCE_KIND}/resources?hasOwner=false&isInScope=true&pageSize=100. Page using results.pageInfo.endCursor exactly as above. Do NOT assume a short page is the last page — only hasNextPage=false ends pagination. Collect each entry's resourceId and displayName.

STEP 3 — Bulk update in batches of 50.
PATCH /v1/integrations/{INTEGRATION_ID}/resource-kinds/{RESOURCE_KIND}/resources with body { "updates": [{ id, ownerId: OWNER_ID, description: <rendered template> }, ...] }. The endpoint accepts AT MOST 50 entries per call — chunk strictly. Issue chunks sequentially (no parallelism) to keep ordering deterministic and stay under rate limits.

ERROR HANDLING (per HTTP status):
- 401 → token expired or missing scope. Exit 2 with "Mint a fresh Manage Vanta token with vanta-api.all:read and vanta-api.all:write."
- 403 → token missing the :write scope. Exit 3.
- 404 → INTEGRATION_ID or RESOURCE_KIND wrong (they are case-sensitive). Exit 5.
- 429 → respect Retry-After header; sleep and retry up to 3 times before failing.
- 5xx → exponential backoff 1s, 2s, 4s; fail after 3 attempts.
- 200 from PATCH → still parse results[]: each entry is { id, status: "SUCCESS" | "ERROR", message? }. Successful entries are committed even if siblings failed. Split into success/failure arrays; never retry SUCCESS ids.

OUTPUT:
- Print one line per attempted update: resourceId | displayName | status | message.
- Print a summary: "Updated X/Y resources across Z batches. Failed: F. Skipped (already owned): S."
- Exit 0 only if F == 0; otherwise exit 1.

DEFINITION OF DONE:
- A second run reports 0 candidates from Step 2.
- Every failure carries a human-actionable message.
- The script never overwrites a non-null existing owner.
- The bearer token never appears in logs or stdout.
- Total request volume is predictable: 1 + ceil(people/100) + ceil(resources/100) + ceil(resources/50) calls.

CONSTRAINTS:
- Use the language of this repository and only the standard library plus one HTTP client.
- No retries on 4xx other than 429.
- No more than 50 ids per PATCH, ever.`}
  />
</VibePrompts>

## Next steps

<CardGroup cols={2}>
  <Card title="Scope resources in or out" icon="filter" href="/docs/guides/scope-resources-at-the-integration-level">
    Mark resources `inScope: false` to exclude them from tests entirely.
  </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 the controls these resources support.
  </Card>

  <Card title="Try it in Postman" icon="paper-plane" href="/docs/postman-setup">
    Import the collection and run the bulk 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 — controls, tests, documents, people, integrations.
  </Card>
</CardGroup>
