> ## 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 public integration

> Become a Vanta partner, complete the OAuth authorization-code flow, and push your first resource into a customer's Vanta tenant.

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="30 minutes" />

You'll create a `UserAccount` record visible on the **Access** page, your app listed under **Settings → Integrations**, and Vanta's standard compliance tests for user accounts running against the data you pushed.

## Before you begin

Make sure you have:

* [Vanta partner account](/docs/guides/become-partner).
* A web app with a server-side component that can complete an OAuth authorization-code flow.
* A publicly reachable HTTPS redirect URI (HTTP is allowed only for `localhost` and `127.0.0.1` during development).

<Info>
  Building a **private** integration to connect a homegrown app, on-prem system, or unsupported SaaS tool to your own Vanta tenant? Private integrations are single-tenant and use the simpler `client_credentials` grant — skip the OAuth steps and follow the [Build a Private Integration quickstart](/docs/quickstart/build-private-integration) instead.
</Info>

<Note>
  **Custom Tests are not available to public integrations.** Public integrations push [resources](/docs/concepts/resources) into customer tenants and Vanta runs its [built-in tests](/docs/concepts/tests#built-in-tests-vs-custom-tests) against them automatically. [Custom Tests](/docs/concepts/tests#custom-tests) are a customer-tenant-only feature — partners can't author them on a customer's behalf, and there is no Build Integrations API for creating them. If you ship a [custom resource type](/docs/concepts/resources#custom-resources), each customer writes their own Custom Test against that data in their own dashboard.
</Note>

<Steps titleSize="h3">
  <Step title="Create a Build Integrations application">
    **Vanta Dashboard** — sign in to the Vanta app with the partner email Vanta provided (`[your_email]+connectors@[your_company].com`). Vanta sends a magic link; check spam if you don't see it.

    Open [Settings → Developer Console](https://app.vanta.com/settings/developer-console) and click **Create**. Choose **Build Integrations** as the app type, then select **Public** for the distribution. Fill in:

    * **Application name** — what customers see in the Vanta marketplace.
    * **Application description** — a brief description of your integration.
    * **Application category** — determines marketplace placement.
    * **Uploads documents** — check this if your integration uploads file-based evidence.
    * **Application URL** — the URL where users access your tool (e.g. `app.yourtool.com`, not the marketing page).
    * **Installation URL** — where the marketplace sends customers to start the OAuth flow. **Not** the same as your `redirect_uri`.

          <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 Public 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 on your server. You can rotate the secret at any time without invalidating active customer tokens.
  </Step>

  <Step title="Register a redirect URI">
    **Vanta Dashboard** — in the Developer Console, register one or more redirect URIs for your application. When a user authorizes your integration, Vanta sends them back to one of these URIs with an authorization code.

    <Info>
      **URI format requirements**

      * Must be HTTPS, except for `localhost` and `127.0.0.1` (HTTP allowed for development).
      * Custom ports are allowed for `localhost`/`127.0.0.1`.
      * May contain query parameters but **must not** contain URL fragments (`#`).

      Examples: `https://example.com/oauth/callback/vanta`, `http://localhost:8000/oauth/callback/vanta`, `http://127.0.0.1/callback?some-param=value`.
    </Info>
  </Step>

  <Step title="Initiate the OAuth flow">
    From **your server**, redirect the user's browser to `/oauth/authorize` on **the customer's regional Vanta app host** — this host is regional even though the API host (`api.vanta.com`) isn't:

    | Region        | App host                    |
    | ------------- | --------------------------- |
    | United States | `https://app.vanta.com`     |
    | Europe        | `https://app.eu.vanta.com`  |
    | Australia     | `https://app.aus.vanta.com` |

    <Warning>
      Sending an EU or AU customer to `app.vanta.com` instead of their own region's host can break authorization.
    </Warning>

    A complete authorization URL (US example) looks like:

    ```text theme={"system"}
    https://app.vanta.com/oauth/authorize?client_id=vci_your_client_id&scope=connectors.self:write-resource%20connectors.self:read-resource&state=xyzABC123&redirect_uri=https://partner-app.com/oauth/callback&source_id=acct1234&response_type=code
    ```

    Query parameters:

    | Parameter       | Description                                                                                                                                                                           |
    | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `client_id`     | From the Developer Console.                                                                                                                                                           |
    | `scope`         | Space-separated list of scopes (see below).                                                                                                                                           |
    | `state`         | An opaque random string you generate, store in the user's session, and compare on callback. Confirms the response came from the flow you started (CSRF protection).                   |
    | `redirect_uri`  | Must match a redirect URI registered in Step 2.                                                                                                                                       |
    | `source_id`     | Unique identifier for the user's account in **your** application (e.g. `accountId`). Allows multiple of your accounts to connect to the same Vanta account. Make this human-readable. |
    | `response_type` | Always `code`.                                                                                                                                                                        |

    This quickstart requests two scopes:

    * `connectors.self:write-resource` — send resources to Vanta on behalf of users.
    * `connectors.self:read-resource` — read resources you previously sent (used in Step 7 to verify).

    The user sees a Vanta authorization screen, clicks **Allow**, and Vanta redirects them to your `redirect_uri` with `code` and `state` query parameters appended. The integration also appears in the customer's tenant under **Settings → Integrations** with the application name you set in Step 1.

    <Warning>
      **Verify `state` on callback.** Compare the returned `state` against the value you stored in the user's session before exchanging the code. If they don't match, abort the flow.
    </Warning>

    <AccordionGroup>
      <Accordion title="Got an OAuth error code on redirect?">
        If Vanta redirects with an `error` query param, the code identifies the failure:

        | Code | Cause                                                               |
        | ---- | ------------------------------------------------------------------- |
        | 1    | The initial redirect to Vanta is missing required query parameters. |
        | 2    | Invalid scopes.                                                     |
        | 3    | Invalid `responseType` (must be `code`).                            |
        | 4    | Error loading the registered application.                           |
        | 5    | Application not found — check `client_id`.                          |
        | 6    | `redirect_uri` is not registered in the Developer Console.          |
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Exchange the code for an access token">
    On **your server**, exchange the authorization `code` for an access token within **30 seconds** of receiving it — after that the code expires and the user must reauthorize.

    <CodeGroup>
      ```bash Terminal theme={"system"}
      curl --location 'https://api.vanta.com/oauth/token' \
        --header 'Content-Type: application/json' \
        --data '{
          "client_id": "vci_your_client_id",
          "client_secret": "vcs_your_client_secret",
          "code": "vac_authorization_code_from_callback",
          "source_id": "acct1234",
          "redirect_uri": "https://partner-app.com/oauth/callback",
          "grant_type": "authorization_code"
        }'
      ```

      ```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: process.env.VANTA_CLIENT_ID,
          client_secret: process.env.VANTA_CLIENT_SECRET,
          code,
          source_id: accountId,
          redirect_uri: "https://partner-app.com/oauth/callback",
          grant_type: "authorization_code",
        }),
      });
      const tokens = await response.json();
      ```

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

      response = requests.post(
          "https://api.vanta.com/oauth/token",
          json={
              "client_id": client_id,
              "client_secret": client_secret,
              "code": code,
              "source_id": account_id,
              "redirect_uri": "https://partner-app.com/oauth/callback",
              "grant_type": "authorization_code",
          },
      )
      tokens = response.json()
      ```
    </CodeGroup>

    Expected response:

    ```json theme={"system"}
    {
      "access_token": "your_access_token",
      "refresh_token": "your_refresh_token",
      "expires_in": 3600,
      "token_type": "Bearer"
    }
    ```

    Both `access_token` and `refresh_token` are sensitive — store them securely, scoped per user. When the access token expires (1 hour), exchange the `refresh_token` for a new pair via the same endpoint with `grant_type: "refresh_token"`. The previous refresh token expires 3 hours after first use to refresh — make sure you persist the new one.

    <Warning>
      Configure automatic retries on 5xx errors and network failures, or your user may need to re-authorize.
    </Warning>

    <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. Most off-the-shelf OAuth libraries default to `application/x-www-form-urlencoded`, so you may need to override that.
      </Accordion>

      <Accordion title="Got an invalid_grant error?">
        Authorization codes expire after 30 seconds. If you took longer than that to exchange the code, restart the flow from Step 3. The `redirect_uri` you send here must also exactly match the one you used to request the `code`.
      </Accordion>
    </AccordionGroup>
  </Step>

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

    From **your server**, replace `your_token_here` with the customer's `access_token` from Step 4 and 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 '{
          "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"
            }
          ]
        }'
      ```

      ```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({
            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={
              "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 for that customer. 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>

    <Info>
      **Tests are generated for you.** As soon as you push a base resource type (e.g. `UserAccount`, `Computer`, `Vulnerability`), Vanta automatically creates and runs the standard [built-in tests](/docs/concepts/tests#built-in-tests-vs-custom-tests) for that resource in the customer's tenant — there's nothing to wire up. Authoring [Custom Tests](/docs/concepts/tests#custom-tests) is **not available to public integrations** — each customer writes their own against the data you push.
    </Info>

    Push resources on a **periodic schedule** (typically hourly) for each customer's tenant to stay current. See per-type schemas in the [Build Integrations API reference](/reference/build-integrations/overview).

    <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. Refresh per Step 4 and retry — if that still fails, re-run the OAuth flow from Step 3 with the correct scopes.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Disconnect when a user uninstalls">
    When a user disconnects your integration in your app, **your server** must call the Suspend API so Vanta cleans up its side and revokes the user's tokens.

    <CodeGroup>
      ```bash Terminal theme={"system"}
      curl --request POST 'https://api.vanta.com/v1/oauth/token/suspend' \
        --header 'Content-Type: application/json' \
        --data '{
          "token": "your_access_or_refresh_token",
          "client_id": "your_client_id",
          "client_secret": "your_client_secret"
        }'
      ```

      ```javascript Node.js theme={"system"}
      await fetch("https://api.vanta.com/v1/oauth/token/suspend", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          token: tokenToRevoke,
          client_id: process.env.VANTA_CLIENT_ID,
          client_secret: process.env.VANTA_CLIENT_SECRET,
        }),
      });
      ```

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

      requests.post(
          "https://api.vanta.com/v1/oauth/token/suspend",
          json={
              "token": token_to_revoke,
              "client_id": client_id,
              "client_secret": client_secret,
          },
      )
      ```
    </CodeGroup>

    Expected response (`200 OK` — idempotent; same response if the token is already revoked):

    ```json theme={"system"}
    {}
    ```

    <AccordionGroup>
      <Accordion title="Got a 401 Unauthorized?">
        The token doesn't belong to this `client_id`. Confirm you're sending the credentials for the same application that minted the token, not a different app in your Developer Console.

        ```json theme={"system"}
        {
          "message": "Token does not belong to this application",
          "statusCode": 401
        }
        ```
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Verify it worked">
    From your **terminal**, read back the resource you pushed in Step 5 to confirm the full flow — auth, write, and read — is wired correctly.

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

    Then open the customer's Vanta app and navigate to the **[Access page](https://app.vanta.com/access/accounts)** — the user you `PUT` should appear with the same `displayName` and `email`, and your integration should be listed under **Settings → Integrations**.

    | Scenario     | Test input                                                                                                       | Expected result                                                                                                                                 |
    | ------------ | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
    | Success      | Valid `access_token` after the PUT in Step 5                                                                     | `200 OK`, response `data` includes `user_123`; the user appears in the customer's Vanta dashboard                                               |
    | Auth failure | Expired or revoked `access_token`                                                                                | `401 Unauthorized` — refresh per Step 4 and retry                                                                                               |
    | Edge case    | Refresh-token rotation: exchange `refresh_token` for a new pair, then repeat the GET with the new `access_token` | `200 OK`, same `data` — confirms rotation works without re-authorization                                                                        |
    | 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 `GET` results — confirms the full-state replacement model from Step 5 |
  </Step>
</Steps>

## Congratulations

You have a working public integration — OAuth-authorized by a customer, pushing resources into their Vanta tenant, and verifiable via a read-back. From here you can:

* **Schedule it.** Run the sync hourly per customer from cron, a queue worker, or your CI runner. Always refresh the access token at the start of each run.
* **Expand resource coverage.** Push additional supported types (`Computer`, `Vulnerability`, `BackgroundCheck`, `TrainingRecord`, etc.) from the same app.
* **Harden refresh** with automatic retries on 5xx so users don't have to re-authorize after transient failures.
* **List your integration** in the Vanta marketplace once you're ready for customers to discover and install it.

## Next steps

<CardGroup cols={2}>
  <Card title="List your integration" href="/docs/guides/list-your-integration">
    Submit your integration for review and publish to the Vanta marketplace.
  </Card>

  <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="Resources" href="/docs/concepts/resources">
    Deep dive on the resource lifecycle, including custom resources.
  </Card>

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