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

# Build a custom integration for compliance data

> Push user accounts and custom resources from a homegrown app into Vanta, then turn that data into pass/fail evidence with a Custom Test.

export const EstimatedTime = ({time, icon = "clock"}) => <div className="vanta-estimated-time" style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.5rem",
  padding: "0.35rem 0.75rem",
  marginBottom: "1rem",
  borderRadius: "9999px",
  background: "color-mix(in srgb, #5E05C4 10%, transparent)",
  border: "1px solid color-mix(in srgb, #5E05C4 25%, transparent)",
  fontSize: "0.8125rem",
  fontWeight: 500,
  lineHeight: 1,
  color: "#5E05C4",
  whiteSpace: "nowrap"
}}>
    <Icon icon={icon} iconType="regular" size={14} color="#5E05C4" />
    <span>Estimated time: {time}</span>
  </div>;

<EstimatedTime time="20 minutes" />

By the end of this quickstart you'll have a private integration that pushes user accounts and custom resources into your Vanta tenant, with a Custom Test turning that data into pass/fail compliance evidence.

## Before you begin

Make sure you have:

* A Vanta account with admin access.
* A terminal or HTTP client (cURL, Postman, or your language of choice).

<Note>
  Building an integration for multiple Vanta organizations? Private integrations are single-tenant and use the simpler `client_credentials` grant. If you want other Vanta organizations to install your integration, follow the [Build a Public Integration quickstart](/docs/quickstart/build-integration) instead.
</Note>

<Steps titleSize="h3">
  <Step title="Create a Private Integration">
    **Vanta Dashboard** — sign in to the Vanta app, open [Settings → Developer Console](https://app.vanta.com/settings/developer-console), and click **Create**.

    Choose **Build Integrations** as the app type, then select **Private** for the distribution. Fill in:

    * **Application name** — `Demo Private Integration` (or enter a name of your choosing).
    * **Application description** — `Vanta quickstart demo integration`

          <img src="https://mintcdn.com/vanta/WstAJ2TKBLS7hAXq/images/81b2284-Screenshot_2024-07-07_at_6.30.03_PM.png?fit=max&auto=format&n=WstAJ2TKBLS7hAXq&q=85&s=faf8033b496f007cd17c6c4b20d5af5d" alt="Vanta Developer Console showing the Build Integrations app creation form with Private distribution selected" width="2482" height="1422" data-path="images/81b2284-Screenshot_2024-07-07_at_6.30.03_PM.png" />

    The OAuth `client_id` is auto-generated. Click **Generate client secret** to create the secret. Store both values securely. You can rotate the secret at any time without invalidating data you've already pushed.
  </Step>

  <Step title="Get an access token">
    From your **terminal**, exchange your client credentials for an access token. This quickstart requests two scopes: `connectors.self:write-resource` to push data and `connectors.self:read-resource` to read it back for verification.

    <CodeGroup>
      ```bash Terminal theme={"system"}
      curl --location 'https://api.vanta.com/oauth/token' \
        --header 'Content-Type: application/json' \
        --data '{
          "client_id": "your_client_id",
          "client_secret": "your_client_secret",
          "scope": "connectors.self:write-resource connectors.self:read-resource",
          "grant_type": "client_credentials"
        }'
      ```

      ```javascript Node.js theme={"system"}
      const response = await fetch("https://api.vanta.com/oauth/token", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          client_id: "your_client_id",
          client_secret: "your_client_secret",
          scope: "connectors.self:write-resource connectors.self:read-resource",
          grant_type: "client_credentials",
        }),
      });
      const { access_token } = await response.json();
      ```

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

      response = requests.post(
          "https://api.vanta.com/oauth/token",
          json={
              "client_id": "your_client_id",
              "client_secret": "your_client_secret",
              "scope": "connectors.self:write-resource connectors.self:read-resource",
              "grant_type": "client_credentials",
          },
      )
      access_token = response.json()["access_token"]
      ```
    </CodeGroup>

    Expected response:

    ```json theme={"system"}
    {
      "access_token": "vat_your_token",
      "expires_in": 3600,
      "token_type": "Bearer"
    }
    ```

    <Info>
      **One token at a time.** Vanta only allows one active access token per application — requesting a new token immediately revokes the previous one. Tokens expire after one hour, so most integrations request a fresh token at the top of each sync run.
    </Info>

    <AccordionGroup>
      <Accordion title="Got a 401 or invalid_client?">
        Double-check the `client_id` and `client_secret` from the Developer Console. If you rotated the secret, the old one is no longer valid. Make sure your `Content-Type` header is `application/json` — Vanta's `/oauth/token` does not accept form-encoded bodies.
      </Accordion>

      <Accordion title="Got an invalid_scope error?">
        You requested a scope that isn't available to **Build Integrations** apps. Private integrations can request `connectors.self:write-resource`, `connectors.self:read-resource`, `self:write-document`, and `self:read-document`. Request only what your tool needs.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Push your first resource">
    Most private integrations start with `UserAccount` — Vanta's representation of a person in an external system, used for access reviews. `PUT` is idempotent on `uniqueId`, so it's safe to retry on network errors.

    1. **Vanta Dashboard** — in your private app's **Resources** tab, click **Add resource**, choose the **User Account** base type, save, and copy the generated `Resource ID`. Every `PUT` references this ID so Vanta knows which resource configuration the records belong to.

           <img src="https://mintcdn.com/vanta/WstAJ2TKBLS7hAXq/images/create-resource-screenshot.png?fit=max&auto=format&n=WstAJ2TKBLS7hAXq&q=85&s=d6954f51170523a214f822847d523d99" alt="Resources tab in the Vanta app showing the Add resource flow with the User Account base type selected" width="453" height="656" data-path="images/create-resource-screenshot.png" />

    2. **Terminal** — replace `your_token_here` with the `access_token` from Step 2 and `your_resource_id` with the `Resource ID` you just copied, then push a record:

    <CodeGroup>
      ```bash Terminal theme={"system"}
      curl --request PUT 'https://api.vanta.com/v1/resources/user_account' \
        --header 'Authorization: Bearer your_token_here' \
        --header 'Content-Type: application/json' \
        --data '{
          "resourceId": "your_resource_id",
          "resources": [
            {
              "uniqueId": "user_123",
              "displayName": "Jane Doe",
              "fullName": "Jane Doe",
              "accountName": "jane.doe",
              "email": "developers@vanta.com",
              "status": "ACTIVE",
              "mfaEnabled": true,
              "mfaMethods": ["SMS", "EMAIL"],
              "authMethod": "SSO",
              "permissionLevel": "ADMIN",
              "externalUrl": "https://app.example.com/users/user_123",
              "createdTimestamp": "2026-05-05T00:00:00Z",
              "updatedTimestamp": "2026-05-05T00:00:00Z"
            }
          ]
        }'
      ```

      ```javascript Node.js theme={"system"}
      const response = await fetch(
        "https://api.vanta.com/v1/resources/user_account",
        {
          method: "PUT",
          headers: {
            Authorization: `Bearer ${access_token}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            resourceId: "your_resource_id",
            resources: [
              {
                uniqueId: "user_123",
                displayName: "Jane Doe",
                fullName: "Jane Doe",
                accountName: "jane.doe",
                email: "[email protected]",
                status: "ACTIVE",
                mfaEnabled: true,
                mfaMethods: ["SMS", "EMAIL"],
                authMethod: "SSO",
                permissionLevel: "ADMIN",
                externalUrl: "https://app.example.com/users/user_123",
                createdTimestamp: "2026-05-05T00:00:00Z",
                updatedTimestamp: "2026-05-05T00:00:00Z",
              },
            ],
          }),
        }
      );
      ```

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

      response = requests.put(
          "https://api.vanta.com/v1/resources/user_account",
          headers={
              "Authorization": f"Bearer {access_token}",
              "Content-Type": "application/json",
          },
          json={
              "resourceId": "your_resource_id",
              "resources": [
                  {
                      "uniqueId": "user_123",
                      "displayName": "Jane Doe",
                      "fullName": "Jane Doe",
                      "accountName": "jane.doe",
                      "email": "[email protected]",
                      "status": "ACTIVE",
                      "mfaEnabled": True,
                      "mfaMethods": ["SMS", "EMAIL"],
                      "authMethod": "SSO",
                      "permissionLevel": "ADMIN",
                      "externalUrl": "https://app.example.com/users/user_123",
                      "createdTimestamp": "2026-05-05T00:00:00Z",
                      "updatedTimestamp": "2026-05-05T00:00:00Z",
                  }
              ],
          },
      )
      ```
    </CodeGroup>

    Expected response (`200 OK`):

    ```json theme={"system"}
    {
      "results": {
        "accepted": 1,
        "rejected": 0
      }
    }
    ```

    <Warning>
      Each `PUT` represents the **full state** of resources you own. Any `uniqueId` you previously pushed but omit from a later `PUT` is marked as no longer existing in Vanta. Sync the complete set on every run.
    </Warning>

    <AccordionGroup>
      <Accordion title="Got a 401 Unauthorized?">
        Your access token has expired (tokens last one hour) or your app wasn't granted the `connectors.self:write-resource` scope. Re-run Step 2 with the scopes shown above.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Sync custom resources">
    Use a **custom resource** to define your own schema and push data Vanta doesn't model out of the box — homegrown app objects, on-prem servers, internal review records, and the like.

    1. **Vanta Dashboard** — open your private app's **Resources** tab and create a new resource using the **Custom Resource** base type. Give it a name (e.g. `Internal Server`) and define the custom fields you want to sync to Vanta using JSON Type Definition.

       ```json theme={"system"}
       {
         "properties": {
           "name":        { "type": "string" },
           "active":      { "type": "boolean" },
           "memory":      { "type": "int32" },
           "lastUpdated": { "type": "timestamp" }
         }
       }
       ```

       Save and copy the generated `Resource ID` — you'll reference it on every push.

    2. **Terminal** — push records to the custom-resource endpoint. Replace `your_token_here` with your `access_token` and `your_custom_resource_id` with the `Resource ID` you just copied. Each record includes the base fields (`displayName`, `uniqueId`, `externalUrl`) at the top level and your schema fields nested inside `customProperties`.

    <CodeGroup>
      ```bash Terminal theme={"system"}
      curl --request PUT 'https://api.vanta.com/v1/resources/custom_resource' \
        --header 'Authorization: Bearer your_token_here' \
        --header 'Content-Type: application/json' \
        --data '{
          "resourceId": "your_custom_resource_id",
          "resources": [
            {
              "displayName": "auth-prod-1",
              "uniqueId": "srv_001",
              "externalUrl": "https://app.example.com/servers/srv_001",
              "customProperties": {
                "name": "auth-prod-1",
                "active": true,
                "memory": 16384,
                "lastUpdated": "2026-05-05T00:00:00Z"
              }
            }
          ]
        }'
      ```

      ```javascript Node.js theme={"system"}
      const response = await fetch(
        "https://api.vanta.com/v1/resources/custom_resource",
        {
          method: "PUT",
          headers: {
            Authorization: `Bearer ${access_token}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            resourceId: "your_custom_resource_id",
            resources: [
              {
                displayName: "auth-prod-1",
                uniqueId: "srv_001",
                externalUrl: "https://app.example.com/servers/srv_001",
                customProperties: {
                  name: "auth-prod-1",
                  active: true,
                  memory: 16384,
                  lastUpdated: "2026-05-05T00:00:00Z",
                },
              },
            ],
          }),
        }
      );
      ```

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

      response = requests.put(
          "https://api.vanta.com/v1/resources/custom_resource",
          headers={
              "Authorization": f"Bearer {access_token}",
              "Content-Type": "application/json",
          },
          json={
              "resourceId": "your_custom_resource_id",
              "resources": [
                  {
                      "displayName": "auth-prod-1",
                      "uniqueId": "srv_001",
                      "externalUrl": "https://app.example.com/servers/srv_001",
                      "customProperties": {
                          "name": "auth-prod-1",
                          "active": True,
                          "memory": 16384,
                          "lastUpdated": "2026-05-05T00:00:00Z",
                      },
                  }
              ],
          },
      )
      ```
    </CodeGroup>

    <Info>
      Custom resources don't have built-in compliance tests — Vanta doesn't know what "compliant" means for your schema yet. Step 5 walks through authoring a Custom Test to define that pass/fail logic.
    </Info>

    <AccordionGroup>
      <Accordion title="Got `resourceId not found`?">
        The `resourceId` in your `PUT` body must match the ID Vanta generated when you created the custom resource in the Dashboard. Open the resource definition in the **Resources** tab and copy the ID exactly — it's case-sensitive.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Author a Custom Test on your custom resource">
    The custom resource from Step 4 is now syncing data, but Vanta doesn't yet know what "compliant" means for your schema — until you author a Custom Test, the records won't contribute to any framework. Custom Tests are authored in the Vanta Dashboard; there is no API to create them.

    <Info>
      Custom Tests may require a plan upgrade or add-on. See [Vanta Plans and Pricing](https://www.vanta.com/pricing) or contact your Customer Success team if the **+ Create custom test** button isn't visible on the Tests page.
    </Info>

    1. **Vanta Dashboard** — open the Vanta app, navigate to the **Tests** page, and click **+ Create custom test**.

    2. Fill in the test metadata:

       * **Test name** — e.g. `Internal servers have ≥ 8 GiB RAM and are active`.
       * **Description** — what the test verifies, in business terms (this is what auditors read).
       * **How to fix / remediate** — instructions for whoever owns a failing resource.
       * **Integration** — select the private integration you created in Step 1. The resource picker will then show every resource type that app syncs, including the custom resource from Step 4.

    3. **Pick the resource type** — choose the custom resource you defined in Step 4 (e.g. `Internal Server`).

    4. Use the logic builder to define the **pass/fail rule** for each record. Define the resource outcome as follows:

       ```text theme={"system"}
       active equals true
       AND memory >= 8192
       ```

       Each record you `PUT` is evaluated against this rule independently. If any record fails, the test fails for that resource and contributes to your framework gap.

           <img src="https://mintcdn.com/vanta/WstAJ2TKBLS7hAXq/images/create-test-screenshot.png?fit=max&auto=format&n=WstAJ2TKBLS7hAXq&q=85&s=48086720635f91a57960bcfe4b3f5cc6" alt="Custom Test builder in the Vanta app showing the resource picker, pass/fail logic, and evaluation preview" width="1666" height="1818" data-path="images/create-test-screenshot.png" />

    5. Review the **evaluation preview**. Vanta runs the test against the records you've already synced and shows pass/fail counts plus a per-resource breakdown. If nothing appears, confirm Step 4's `PUT` returned `accepted: 1` (or higher).

    6. Click **Create** to save the test.

    7. Finalize the test so it counts toward your compliance program:

       * **Map it to controls.** Open the test, go to the **Controls** tab, click **Add control**, and select the controls (and therefore frameworks) the test should satisfy.
       * **Assign an SLA category** so failing resources have a remediation deadline.
       * **Assign a test owner** so failures route to the right person.

    <Warning>
      **Custom Tests are immutable after creation.** If you need to change the logic, name, or description later, copy the test and edit the copy — Vanta keeps the original for audit trail purposes. Lock in your rule before mapping to controls.
    </Warning>

    <AccordionGroup>
      <Accordion title="My custom resource doesn't appear in the resource picker">
        The Custom Test builder only shows resource types that have at least one record synced. Re-run the `PUT` from Step 4 to push at least one record, then refresh the **+ Create custom test** flow. If it still doesn't appear, confirm you selected the correct integration (Step 1's private app) — custom resources are scoped per-app.
      </Accordion>

      <Accordion title="The evaluation preview shows 0 resources">
        Either no records have been synced yet, or your scoping conditions filter them all out. Remove scoping conditions temporarily to confirm records exist, then re-add them one at a time to find the offending filter.
      </Accordion>

      <Accordion title="The test is passing but I expected it to fail (or vice versa)">
        Custom Test logic is evaluated **per record**. By default, every record must pass for the test to pass (one failing record fails the whole test), but the reducer is configurable when you author the test — confirm you picked the one you intended. Open the per-resource breakdown in the test view to see which records are flagged and why.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Verify it worked">
    From your **terminal**, read back the resource you pushed in Step 3 to confirm you completed everything correctly.

    ```bash Terminal theme={"system"}
    curl 'https://api.vanta.com/v1/resources/user_account?resourceId=your_resource_id&pageSize=10' \
      --header 'Authorization: Bearer your_token_here' \
      --header 'Accept: application/json'
    ```

    Then open the Vanta app:

    * Navigate to the **[Access page](https://app.vanta.com/access/accounts)**. The user you `PUT` should appear with the same `displayName` and `email`.
    * Open **Tests → Custom**, find the test you authored in Step 5, and confirm it shows the pass/fail outcome you expected for each record.

    | Scenario            | Test input                                                                 | Expected result                                                                                                                                                   |
    | ------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | Success             | Valid `access_token` after the PUT in Step 3                               | `200 OK`, response `data` includes `user_123`; the user appears in the Vanta dashboard                                                                            |
    | Custom Test success | Custom resource records pushed in Step 4 that satisfy the rule from Step 5 | Test result page shows **Passing** with each record listed under "Passing resources"                                                                              |
    | Auth failure        | Expired `access_token` (older than one hour)                               | `401 Unauthorized` — re-run Step 2 for a fresh token                                                                                                              |
    | Edge case           | A `uniqueId` you previously pushed but omitted from the latest `PUT`       | The resource is marked as no longer existing in Vanta and disappears from the Custom Test's evaluated set — confirms the full-state replacement model from Step 3 |
  </Step>
</Steps>

## Congratulations

You have a working private integration — authenticating with `client_credentials`, pushing both built-in and custom resources into your own Vanta tenant, and evaluating that data against compliance criteria you define via Custom Tests. From here you can:

* **Schedule it.** Run the sync hourly from cron, a Lambda, or your CI runner. Always request a fresh token at the start of the run.
* **Expand resource coverage.** Push additional supported types (`Computer`, `Vulnerability`, `BackgroundCheck`, `TrainingRecord`, etc.) from the same app.
* **Add more Custom Tests.** Layer additional pass/fail rules on the same custom resource, or copy an existing test to create a variant for a different scope.
* **Map tests to frameworks.** Wire each Custom Test to the controls it satisfies so failing resources surface as gaps in SOC 2, ISO 27001, HIPAA, and any other framework you're tracking.

## Next steps

<CardGroup cols={2}>
  <Card title="Supported resource types" href="/reference/build-integrations/overview">
    Full reference of every resource type Vanta accepts out of the box.
  </Card>

  <Card title="Create a custom resource" href="/docs/concepts/resources#custom-resources">
    Deep dive on defining a custom schema and pushing records.
  </Card>

  <Card title="Author a Custom Test" href="https://help.vanta.com/hc/en-us/articles/28012720726036-Creating-Custom-Tests">
    Turn the data you sync into compliance evidence with pass/fail logic.
  </Card>

  <Card title="Build Integrations API reference" href="/reference/build-integrations/overview">
    Every endpoint available to Build Integrations apps.
  </Card>
</CardGroup>
