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

# Vanta API authentication

> How Vanta authenticates API clients, the OAuth grant types each application type uses, and the nuances of tokens, refresh, and revocation.

Every Vanta API request is authenticated with a short-lived **OAuth 2.0 bearer token**. Which OAuth flow you use — and the shape of the credentials you exchange — depends on the **application type** you create in the Developer Console. This page explains the underlying model so you can pick the right flow, understand the tradeoffs, and avoid the foot-guns that aren't obvious from the per-quickstart instructions.

## Overview

Every Vanta API client is registered in the **Developer Console** as an **Application**. Each Application has a `client_id` and a `client_secret`, plus an **app type** that determines which OAuth grant types and scopes it can use. Clients exchange those credentials at a single token endpoint — `POST https://api.vanta.com/oauth/token` — for an `access_token`, then send that token as `Authorization: Bearer <access_token>` on every API request. Tokens last one hour. After that you either request a fresh one (machine-to-machine flows) or refresh it (per-customer OAuth flows).

<Info>
  Vanta does **not** support API keys, basic auth, or session cookies for the API. Every authenticated request goes through the OAuth bearer token model below.
</Info>

## Application types

Vanta exposes four application types, mapped to two OAuth 2.0 grant types. The grant type — not the app type — determines the shape of the auth flow.

| Application type                 | Grant type           | Token is scoped to           | Refresh tokens |
| -------------------------------- | -------------------- | ---------------------------- | -------------- |
| **Manage Vanta**                 | `client_credentials` | Your own Vanta tenant        | No             |
| **Build Integrations** (Private) | `client_credentials` | Your own Vanta tenant        | No             |
| **Build Integrations** (Public)  | `authorization_code` | A specific customer's tenant | Yes            |
| **Auditor API**                  | `client_credentials` | Your auditor firm's audits   | No             |

<Tabs>
  <Tab title="Client Credentials">
    Used by **Manage Vanta**, **Build Integrations** (Private), and **Auditor API** apps. There is no end-user; your server holds the `client_id` and `client_secret` and exchanges them directly for an access token.

    ```http theme={"system"}
    POST https://api.vanta.com/oauth/token
    Content-Type: application/json

    {
      "client_id": "vci_...",
      "client_secret": "vcs_...",
      "scope": "vanta-api.all:read",
      "grant_type": "client_credentials"
    }
    ```

    Response:

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

    Notice there's **no `refresh_token`** — when the access token expires, you just exchange your client credentials again for a fresh one. Most integrations request a new token at the top of each scheduled run.
  </Tab>

  <Tab title="Authorization Code - Public Integrations">
    Used only by **Build Integrations** (Public) apps (i.e. integrations published in the [Vanta marketplace](https://app.vanta.com/integrations)). A customer clicks **Allow** in their browser, Vanta redirects them back to you with a short-lived `code`, and your server exchanges that `code` for a per-customer `access_token` plus a `refresh_token`.

    The flow:

    1. Your server redirects the customer's browser to `/oauth/authorize` on **the customer's regional app host** (`app.vanta.com`, `app.eu.vanta.com`, or `app.aus.vanta.com` — see [Base URLs and regions](#base-urls-and-regions)) with `client_id`, `scope`, `state`, `redirect_uri`, `source_id`, and `response_type=code`.
    2. The customer authorizes; Vanta redirects to your `redirect_uri` with `code` and `state` query parameters.
    3. Your server `POST`s to `/oauth/token` with `grant_type=authorization_code` to receive both an `access_token` and a `refresh_token`.
    4. When the access token expires, your server `POST`s to `/oauth/token` with `grant_type=refresh_token` to rotate.

    ```http theme={"system"}
    POST https://api.vanta.com/oauth/token
    Content-Type: application/json

    {
      "client_id": "vci_...",
      "client_secret": "vcs_...",
      "code": "vac_...",
      "source_id": "acct1234",
      "redirect_uri": "https://partner-app.com/oauth/callback",
      "grant_type": "authorization_code"
    }
    ```

    Response:

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

    <Warning>
      This is the **only** authentication request that touches the browser-facing app host — and unlike `api.vanta.com`, that host is **regional**: use the customer's own region (`app.vanta.com`, `app.eu.vanta.com`, or `app.aus.vanta.com`), not always `app.vanta.com`. All token exchanges and API calls still go to `api.vanta.com` regardless of the customer's region. See [Base URLs and regions](#base-urls-and-regions).
    </Warning>

    For the full request shape — including the redirect URI rules, error codes, and the `source_id` parameter — see the [Build a Public Integration quickstart](/docs/quickstart/build-integration).
  </Tab>
</Tabs>

## Scopes

All applications authenticate against the same `/oauth/token` endpoint, but the **scopes** you can request depend on your app type. Requesting a scope that doesn't match your app type returns an `invalid_scope` error.

The full per-app scope matrix lives in the [API reference](/reference/overview#api-scopes). At a high level:

* **Manage Vanta apps** — `vanta-api.all:read`, `vanta-api.all:write`, `vanta-api.documents:upload`, plus vendor-specific scopes.
* **Build Integrations apps** (private and public) — `connectors.self:read-resource`, `connectors.self:write-resource`, `self:read-document`, `self:write-document`.
* **Auditor apps** — `auditor-api.audit:read|write`, `auditor-api.auditor:read|write`.

Always request the **minimum** scopes your tool needs. Scopes are validated at token-exchange time, so an over-scoped token is a real attack surface even when most of your code paths only read.

## Token lifecycle

The OAuth flows themselves are standard. The behavior **around** those flows is where most production integrations get bitten. The rules below apply to every app type.

<Warning>
  Vanta only allows a **single active `access_token` per Application**. Requesting a new token with the same `client_id` and `client_secret` immediately revokes the previous one — any in-flight requests using the old token will fail with `401 Unauthorized`.
</Warning>

Concretely, this means:

* **Don't run two processes that both mint tokens** for the same Application. They'll race, mutually invalidate each other, and you'll see intermittent `401`s.
* **For `client_credentials` apps**, request the token at the **start** of a sync run and reuse it for the entire run. Don't refetch per request.
* **For `authorization_code` apps**, this rule applies **per `source_id`** (per customer authorization), not globally — different customers get independent tokens.

### Token expiration

`expires_in` is `3600` seconds (1 hour). After that, requests return `401`. The refresh path differs by grant type:

* **`client_credentials`** — re-exchange your `client_id` and `client_secret` for a new token. There's no refresh token; the credentials *are* the refresh.
* **`authorization_code`** — exchange the `refresh_token` you received during initial authorization for a new `access_token` / `refresh_token` pair.

### Refresh token rotation

<Note>
  Applies only to **Build Integrations** (Public) apps. `client_credentials` apps have no refresh token — see [Token expiration](#token-expiration) above.
</Note>

Every successful refresh returns a **new** `refresh_token`. The previous refresh token stays valid for **3 hours** after first use, then expires.

<Warning>
  Persist the new `refresh_token` immediately on every refresh. If your refresh handler crashes after issuing the request but before saving the result, the old token still works for up to 3 hours — long enough to recover without forcing the customer to re-authorize, but only if you actually retry.
</Warning>

The 3-hour window is specifically designed to tolerate transient failures. Configure automatic retries on `5xx` responses and network errors during refresh; if you don't, a single bad request can lock a customer out and force a full re-auth.

### Authorization code expiration

<Note>
  Applies only to **Build Integrations** (Public) apps — `client_credentials` apps never exchange a `code`.
</Note>

After Vanta redirects the customer back to your `redirect_uri` with a `code`, you have **30 seconds** to exchange it at `/oauth/token`. After that the code is dead and the customer has to start the OAuth flow over.

In practice this is only a problem if your callback handler is slow (cold-start Lambdas, blocking on a database write before the token exchange). Do the token exchange first, then persist.

### Request body encoding

<Info>
  Vanta's `/oauth/token` endpoint expects a `Content-Type: application/json` body. Most off-the-shelf OAuth client libraries default to `application/x-www-form-urlencoded` per RFC 6749 — you'll need to override that. If you see `invalid_request` or `invalid_client` errors despite using correct credentials, check the body encoding first.
</Info>

### Rate limits

`/oauth/token` is limited to **5 requests per minute** across all app types. This is shared across token issuance, refresh, and (for Public) the suspend endpoint. It's deliberately low because a healthy integration rarely needs more than one token per hour — if you're hitting the limit, you're probably re-minting per request rather than per run.

## Revoking access

<Tabs>
  <Tab title="Client Credentials">
    To revoke access, **rotate the `client_secret`** in the Developer Console (which immediately invalidates any active token issued with the old secret) or delete the Application entirely.
  </Tab>

  <Tab title="Authorization Code - Public Integrations">
    When a customer disconnects your **Build Integrations** (Public) integration on **your** side (uninstall, account deletion, support escalation), you must call the **Suspend API** so Vanta cleans up its side and revokes the token:

    ```http theme={"system"}
    POST https://api.vanta.com/v1/oauth/token/suspend
    Content-Type: application/json

    {
      "token": "<access_or_refresh_token>",
      "client_id": "vci_...",
      "client_secret": "vcs_..."
    }
    ```

    Suspend is **idempotent** — calling it on an already-revoked token returns `200`. Calling it with a token that doesn't belong to your `client_id` returns `401`.
  </Tab>
</Tabs>

## Credential hygiene

Rules that apply across every app type:

* **Never put `client_secret`, `access_token`, or `refresh_token` in source control or client-side code.** All token exchanges must happen on a server you control.
* **Rotate `client_secret` when team members leave** or any time you suspect a leak. Rotation is supported from the Developer Console. For **Build Integrations** (Public) apps specifically, rotating the secret does **not** invalidate active customer access or refresh tokens — only new token mints.

<Note>
  The following two rules apply only to **Build Integrations** (Public) apps, since non-public apps have no per-customer tokens and never touch the browser redirect.
</Note>

* **Store per-customer tokens encrypted at rest.** A breach of one customer's tokens shouldn't compromise others.
* **Treat `state` validation as mandatory.** Compare the returned `state` against the value you stored in the user's session before exchanging the code; abort if they don't match. This is your CSRF protection.

## Base URLs and regions

The **API host** and the **browser-facing app host** follow different rules — don't assume they're regional the same way.

**Token exchange and API calls always go to the same host**, regardless of the tenant's region:

* `https://api.vanta.com` for standard Vanta tenants — US, EU, and AU alike
* `https://api.vanta-gov.com` for **Vanta Gov** tenants

<Warning>
  `api.eu.vanta.com` and `api.aus.vanta.com` exist but only return an HTTP `308` redirect to `https://api.vanta.com` — don't call them directly. Many HTTP and OAuth libraries don't replay a `POST` body through a redirect, so a token request to the regional hostname can fail with a confusing plain-text `308` response instead of a normal error.
</Warning>

**The browser-facing app host (`/oauth/authorize`, the Developer Console, the customer's dashboard) is regional** — each region is a genuinely separate host, not a redirect:

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

<Warning>
  For **Build Integrations** (Public) apps: redirect the customer to **their own** regional app host, not always `app.vanta.com`. Sending an EU or AU customer to `app.vanta.com` to authorize can fail outright or hit domain/session issues specific to that region.
</Warning>

Everything else — grant types, scopes, token lifetimes, refresh semantics — is identical across regions and deployments.

See the [API reference](/reference/overview#base-urls) for the canonical base URL table.

## Common error patterns

| Symptom                                                                      | Likely cause                                                                                                                                                                                                                                         | Applies to                           |
| ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `401 invalid_client` on token exchange                                       | Wrong `client_id` / `client_secret`, or rotated secret with stale value cached.                                                                                                                                                                      | All apps                             |
| `400 invalid_request` on token exchange                                      | Body sent as `application/x-www-form-urlencoded` instead of `application/json`.                                                                                                                                                                      | All apps                             |
| `400 invalid_scope`                                                          | Requested scope isn't allowed for this app type.                                                                                                                                                                                                     | All apps                             |
| Intermittent `401` mid-run                                                   | Two processes minting tokens for the same Application — they're invalidating each other. Centralize token issuance.                                                                                                                                  | All apps                             |
| `401` after token refresh                                                    | Your refresh handler isn't persisting the **new** `refresh_token`. Without persistence you fall back to the old one, which expires 3 hours after first reuse.                                                                                        | **Build Integrations** (Public) only |
| Customer "uninstalled" but data still flowing                                | You didn't call the Suspend API on disconnect. Wire it into your uninstall handler.                                                                                                                                                                  | **Build Integrations** (Public) only |
| `429` on `/oauth/token`                                                      | You're minting more than 5 tokens/minute — request once per run, not per request.                                                                                                                                                                    | All apps                             |
| `308` / non-JSON response from `/oauth/token`                                | You called a regional hostname (`api.eu.vanta.com`, `api.aus.vanta.com`) instead of `https://api.vanta.com`. Call `api.vanta.com` directly — see [Base URLs and regions](#base-urls-and-regions).                                                    | All apps                             |
| Authorization fails, or a domain/session error on the Vanta authorize screen | You redirected an EU or AU customer to `app.vanta.com` instead of their region's app host (`app.eu.vanta.com` / `app.aus.vanta.com`). The app host is regional even though the API host isn't — see [Base URLs and regions](#base-urls-and-regions). | **Build Integrations** (Public) only |

## Related

<CardGroup cols={2}>
  <Card title="Manage Vanta quickstart" icon="rocket" href="/docs/quickstart/manage-vanta">
    Get a `client_credentials` token and call your first endpoint.
  </Card>

  <Card title="Build a Public Integration" icon="plug" href="/docs/quickstart/build-integration">
    The full `authorization_code` flow, including refresh and Suspend.
  </Card>

  <Card title="Build a Private Integration" icon="cube" href="/docs/quickstart/build-private-integration">
    Single-tenant `client_credentials` flow for homegrown systems.
  </Card>

  <Card title="Conduct an Audit quickstart" icon="clipboard-check" href="/docs/quickstart/conduct-audit">
    Auditor-scoped `client_credentials` flow against customer audits.
  </Card>

  <Card title="API scopes reference" icon="key" href="/reference/overview#api-scopes">
    The full per-app-type scope matrix.
  </Card>

  <Card title="Postman setup" icon="paper-plane" href="/docs/postman-setup">
    Import the collection and start testing requests in seconds.
  </Card>
</CardGroup>
