> ## Documentation Index
> Fetch the complete documentation index at: https://docs.occtoo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Applications

> Use tenant-level machine applications to obtain OAuth tokens for ingest, events, and protected destination APIs.

Applications give backend services a single tenant-level identity for calling Occtoo. An Application is a machine-to-machine OAuth client with a client ID and client secret. You configure its access once, then use the [OAuth 2.0 client credentials flow](https://oauth.net/2/grant-types/client-credentials/) to request short-lived access tokens.

Use an Application for unattended integrations such as services, scheduled jobs, command-line tools, and agents. One Application can ingest data, consume tenant events, and call one or more protected destination APIs.

<Tip>
  **Preferred application model:** Tenant-level Applications are intended to supersede legacy data-provider and per-destination-version applications. They provide one identity and one access model across ingest, events, and destinations, making access easier to configure, review, and change. Use them for new integrations and migrate existing integrations when practical.
</Tip>

<Warning>
  Keep the client secret on a trusted server. Do not put it in browser code, mobile applications, source control, or client-side configuration.
</Warning>

## How access works

Application access has three dimensions:

* **APIs** define which API audiences the Application can request. A protected destination API version has its own audience.
* **Scopes** define what the Application can do in the tenant API, such as write source data or read events.
* **Resources** restrict a capability to Occtoo resources. For example, an Application with `write:sources` can be limited to selected sources or allowed to use every current and future source.

A source ID is not an OAuth scope. You select allowed sources when configuring the Application. The token request only asks for the `write:sources` capability.

For destinations, you can authorize a specific protected API version, every version of one destination, or all destinations. Broader destination selections also apply to matching API versions created later.

| Use case                                 | Audience       | Scope              |
| ---------------------------------------- | -------------- | ------------------ |
| Call a protected destination API version | API version ID | Omit               |
| Ingest data                              | Tenant ID      | `write:sources`    |
| Use every Events API transport           | Tenant ID      | `read:events`      |
| Pull events and inspect metadata         | Tenant ID      | `read:events:pull` |
| Stream events with SSE                   | Tenant ID      | `read:events:sse`  |

<Note>
  Legacy data-provider and per-destination-version credentials remain supported for compatibility while existing integrations migrate.
</Note>

## Request an access token

You need these values from the Application:

* Client ID
* Client secret
* Tenant ID for tenant API access
* API version ID for each protected destination API you want to call

Every direct token request must include an audience. One Application can be authorized for several APIs, but each token targets one audience. Request a separate token for each API you call, using the same client credentials.

The examples below use the production token endpoint, `https://auth.occtoo.com/oauth2/token`. Use the authentication domain provided for your Occtoo environment when working outside production.

<Card title="Try it" icon="play" href="/api-reference/authentication/application-token">
  Request a destination, ingest, or events token in the interactive API playground.
</Card>

Set the values as environment variables before running the examples:

```bash theme={null}
export OCCTOO_CLIENT_ID="<client-id>"
export OCCTOO_CLIENT_SECRET="<client-secret>"
export OCCTOO_TENANT_ID="<tenant-id>"
export OCCTOO_API_VERSION_ID="<api-version-id>"
```

A successful request returns a response similar to:

```json theme={null}
{
  "access_token": "<access-token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "write:sources"
}
```

### Code examples

Each example defines a reusable token helper and requests an ingest token. The commented calls show how to request a destination token, each Events API scope, or all scopes enabled for the tenant audience.

The TypeScript example uses the server-side `fetch` API available in Node.js 18 and later. The Python example requires `requests`. The Rust example requires `reqwest`, `serde`, and `tokio`.

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    request_token() {
      local audience="$1"
      local scope="${2:-}"
      local request=(
        --silent
        --show-error
        --fail-with-body
        --request POST
        --url "https://auth.occtoo.com/oauth2/token"
        --header "Content-Type: application/x-www-form-urlencoded"
        --data-urlencode "grant_type=client_credentials"
        --data-urlencode "client_id=${OCCTOO_CLIENT_ID}"
        --data-urlencode "client_secret=${OCCTOO_CLIENT_SECRET}"
        --data-urlencode "audience=${audience}"
      )

      if [ -n "$scope" ]; then
        request+=(--data-urlencode "scope=${scope}")
      fi

      curl "${request[@]}"
    }

    # Ingest
    request_token "$OCCTOO_TENANT_ID" "write:sources"

    # Destination API version
    # request_token "$OCCTOO_API_VERSION_ID"

    # Events: all transports, pull only, or SSE only
    # request_token "$OCCTOO_TENANT_ID" "read:events"
    # request_token "$OCCTOO_TENANT_ID" "read:events:pull"
    # request_token "$OCCTOO_TENANT_ID" "read:events:sse"

    # Every tenant scope enabled for the Application
    # request_token "$OCCTOO_TENANT_ID"
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    using System.Net.Http.Json;
    using System.Text.Json.Serialization;

    using var http = new HttpClient();

    var clientId = Required("OCCTOO_CLIENT_ID");
    var clientSecret = Required("OCCTOO_CLIENT_SECRET");
    var tenantId = Required("OCCTOO_TENANT_ID");
    var apiVersionId = Required("OCCTOO_API_VERSION_ID");

    // Ingest
    var token = await GetAccessToken(
        http,
        clientId,
        clientSecret,
        tenantId,
        "write:sources");

    var accessToken = token.AccessToken;
    // Send accessToken in the Authorization header. Do not log it.

    // Destination API version:
    // await GetAccessToken(http, clientId, clientSecret, apiVersionId);

    // Events: all transports, pull only, or SSE only:
    // await GetAccessToken(http, clientId, clientSecret, tenantId, "read:events");
    // await GetAccessToken(http, clientId, clientSecret, tenantId, "read:events:pull");
    // await GetAccessToken(http, clientId, clientSecret, tenantId, "read:events:sse");

    // Every tenant scope enabled for the Application:
    // await GetAccessToken(http, clientId, clientSecret, tenantId);

    static async Task<TokenResponse> GetAccessToken(
        HttpClient http,
        string clientId,
        string clientSecret,
        string audience,
        string? scope = null)
    {
        var form = new Dictionary<string, string>
        {
            ["grant_type"] = "client_credentials",
            ["client_id"] = clientId,
            ["client_secret"] = clientSecret,
            ["audience"] = audience
        };

        if (!string.IsNullOrWhiteSpace(scope))
        {
            form["scope"] = scope;
        }

        using var response = await http.PostAsync(
            "https://auth.occtoo.com/oauth2/token",
            new FormUrlEncodedContent(form));

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<TokenResponse>()
            ?? throw new InvalidOperationException("The token response was empty.");
    }

    static string Required(string name) =>
        Environment.GetEnvironmentVariable(name)
        ?? throw new InvalidOperationException($"Missing environment variable: {name}");

    internal sealed record TokenResponse(
        [property: JsonPropertyName("access_token")] string AccessToken,
        [property: JsonPropertyName("expires_in")] int ExpiresIn,
        [property: JsonPropertyName("token_type")] string TokenType);
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    type TokenResponse = {
      access_token: string;
      expires_in: number;
      token_type: string;
      scope?: string;
    };

    const clientId = required("OCCTOO_CLIENT_ID");
    const clientSecret = required("OCCTOO_CLIENT_SECRET");
    const tenantId = required("OCCTOO_TENANT_ID");
    const apiVersionId = required("OCCTOO_API_VERSION_ID");

    // Ingest
    const token = await getAccessToken(tenantId, "write:sources");
    const accessToken = token.access_token;
    // Send accessToken in the Authorization header. Do not log it.

    // Destination API version:
    // await getAccessToken(apiVersionId);

    // Events: all transports, pull only, or SSE only:
    // await getAccessToken(tenantId, "read:events");
    // await getAccessToken(tenantId, "read:events:pull");
    // await getAccessToken(tenantId, "read:events:sse");

    // Every tenant scope enabled for the Application:
    // await getAccessToken(tenantId);

    async function getAccessToken(
      audience: string,
      scope?: string,
    ): Promise<TokenResponse> {
      const form = new URLSearchParams({
        grant_type: "client_credentials",
        client_id: clientId,
        client_secret: clientSecret,
        audience,
      });

      if (scope) {
        form.set("scope", scope);
      }

      const response = await fetch(
        "https://auth.occtoo.com/oauth2/token",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
          },
          body: form,
        },
      );

      if (!response.ok) {
        throw new Error(
          `Token request failed: ${response.status} ${await response.text()}`,
        );
      }

      return (await response.json()) as TokenResponse;
    }

    function required(name: string): string {
      const value = process.env[name];

      if (!value) {
        throw new Error(`Missing environment variable: ${name}`);
      }

      return value;
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import os

    import requests


    CLIENT_ID = os.environ["OCCTOO_CLIENT_ID"]
    CLIENT_SECRET = os.environ["OCCTOO_CLIENT_SECRET"]
    TENANT_ID = os.environ["OCCTOO_TENANT_ID"]
    API_VERSION_ID = os.environ["OCCTOO_API_VERSION_ID"]


    def get_access_token(audience: str, scope: str | None = None) -> dict:
        form = {
            "grant_type": "client_credentials",
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "audience": audience,
        }

        if scope:
            form["scope"] = scope

        response = requests.post(
            "https://auth.occtoo.com/oauth2/token",
            data=form,
            timeout=30,
        )
        response.raise_for_status()
        return response.json()


    # Ingest
    token = get_access_token(TENANT_ID, "write:sources")
    access_token = token["access_token"]
    # Send access_token in the Authorization header. Do not log it.

    # Destination API version:
    # get_access_token(API_VERSION_ID)

    # Events: all transports, pull only, or SSE only:
    # get_access_token(TENANT_ID, "read:events")
    # get_access_token(TENANT_ID, "read:events:pull")
    # get_access_token(TENANT_ID, "read:events:sse")

    # Every tenant scope enabled for the Application:
    # get_access_token(TENANT_ID)
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    use reqwest::Client;
    use serde::Deserialize;
    use std::env;

    #[derive(Deserialize)]
    struct TokenResponse {
        access_token: String,
        expires_in: u64,
        token_type: String,
        scope: Option<String>,
    }

    #[tokio::main]
    async fn main() -> Result<(), reqwest::Error> {
        let client = Client::new();
        let client_id = required("OCCTOO_CLIENT_ID");
        let client_secret = required("OCCTOO_CLIENT_SECRET");
        let tenant_id = required("OCCTOO_TENANT_ID");
        let api_version_id = required("OCCTOO_API_VERSION_ID");

        // Ingest
        let token = get_access_token(
            &client,
            &client_id,
            &client_secret,
            &tenant_id,
            Some("write:sources"),
        )
        .await?;

        let access_token = token.access_token;
        // Send access_token in the Authorization header. Do not log it.

        // Destination API version:
        // get_access_token(&client, &client_id, &client_secret, &api_version_id, None).await?;

        // Events: all transports, pull only, or SSE only:
        // get_access_token(&client, &client_id, &client_secret, &tenant_id, Some("read:events")).await?;
        // get_access_token(&client, &client_id, &client_secret, &tenant_id, Some("read:events:pull")).await?;
        // get_access_token(&client, &client_id, &client_secret, &tenant_id, Some("read:events:sse")).await?;

        // Every tenant scope enabled for the Application:
        // get_access_token(&client, &client_id, &client_secret, &tenant_id, None).await?;

        Ok(())
    }

    async fn get_access_token(
        client: &Client,
        client_id: &str,
        client_secret: &str,
        audience: &str,
        scope: Option<&str>,
    ) -> Result<TokenResponse, reqwest::Error> {
        let mut form = vec![
            ("grant_type", "client_credentials"),
            ("client_id", client_id),
            ("client_secret", client_secret),
            ("audience", audience),
        ];

        if let Some(scope) = scope {
            form.push(("scope", scope));
        }

        client
            .post("https://auth.occtoo.com/oauth2/token")
            .form(&form)
            .send()
            .await?
            .error_for_status()?
            .json::<TokenResponse>()
            .await
    }

    fn required(name: &str) -> String {
        env::var(name).unwrap_or_else(|_| panic!("Missing environment variable: {name}"))
    }
    ```

    This example requires `reqwest`, `serde`, and `tokio`.
  </Tab>
</Tabs>

Cache and reuse the token until shortly before `expires_in` elapses. Then request a new one.

## Access a destination API version

Request a token whose audience is the destination **API version ID**:

In [**Try it**](/api-reference/authentication/application-token), set `audience` to the API version ID and leave `scope` empty.

```bash theme={null}
curl --request POST \
  --url 'https://auth.occtoo.com/oauth2/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode "client_id=${OCCTOO_CLIENT_ID}" \
  --data-urlencode "client_secret=${OCCTOO_CLIENT_SECRET}" \
  --data-urlencode "audience=${OCCTOO_API_VERSION_ID}"
```

Do not use the destination ID as the audience. Each protected API version validates its own audience, so a tenant API token cannot be used to call it. The Application must also be authorized for that API version through its API access configuration.

See [Call a destination API](/get-started/call-a-destination-api) for destination URLs and request examples.

## Access the Ingest API

Request the `write:sources` scope using your Tenant ID as the audience:

In [**Try it**](/api-reference/authentication/application-token), set `audience` to the Tenant ID and `scope` to `write:sources`.

```bash theme={null}
curl --request POST \
  --url 'https://auth.occtoo.com/oauth2/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode "client_id=${OCCTOO_CLIENT_ID}" \
  --data-urlencode "client_secret=${OCCTOO_CLIENT_SECRET}" \
  --data-urlencode "audience=${OCCTOO_TENANT_ID}" \
  --data-urlencode 'scope=write:sources'
```

The Application can ingest only into the [sources](/concepts/source) selected in its resource access configuration. Selecting all sources includes sources created later.

See the [Ingest API reference](/api-reference/ingest/overview) for endpoints and payloads.

## Access the Events API

Use your Tenant ID as the audience. Choose the narrowest scope that covers the transports your integration needs.

In [**Try it**](/api-reference/authentication/application-token), set `audience` to the Tenant ID and use `read:events`, `read:events:pull`, or `read:events:sse` as the scope.

<Tabs>
  <Tab title="All events">
    Use `read:events` for both pull and SSE access.

    ```bash theme={null}
    curl --request POST \
      --url 'https://auth.occtoo.com/oauth2/token' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data-urlencode 'grant_type=client_credentials' \
      --data-urlencode "client_id=${OCCTOO_CLIENT_ID}" \
      --data-urlencode "client_secret=${OCCTOO_CLIENT_SECRET}" \
      --data-urlencode "audience=${OCCTOO_TENANT_ID}" \
      --data-urlencode 'scope=read:events'
    ```
  </Tab>

  <Tab title="Pull">
    Use `read:events:pull` to pull retained events and inspect stream metadata.

    ```bash theme={null}
    curl --request POST \
      --url 'https://auth.occtoo.com/oauth2/token' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data-urlencode 'grant_type=client_credentials' \
      --data-urlencode "client_id=${OCCTOO_CLIENT_ID}" \
      --data-urlencode "client_secret=${OCCTOO_CLIENT_SECRET}" \
      --data-urlencode "audience=${OCCTOO_TENANT_ID}" \
      --data-urlencode 'scope=read:events:pull'
    ```
  </Tab>

  <Tab title="SSE">
    Use `read:events:sse` to stream events with Server-Sent Events.

    ```bash theme={null}
    curl --request POST \
      --url 'https://auth.occtoo.com/oauth2/token' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data-urlencode 'grant_type=client_credentials' \
      --data-urlencode "client_id=${OCCTOO_CLIENT_ID}" \
      --data-urlencode "client_secret=${OCCTOO_CLIENT_SECRET}" \
      --data-urlencode "audience=${OCCTOO_TENANT_ID}" \
      --data-urlencode 'scope=read:events:sse'
    ```
  </Tab>
</Tabs>

See the [Events API reference](/api-reference/events/overview) and [SSE guide](/api-reference/events/streaming) for request and resume behavior.

## Request all granted tenant scopes

The `scope` parameter is optional for the tenant audience. If you omit it, the token contains every tenant API scope enabled for the Application:

```bash theme={null}
curl --request POST \
  --url 'https://auth.occtoo.com/oauth2/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode "client_id=${OCCTOO_CLIENT_ID}" \
  --data-urlencode "client_secret=${OCCTOO_CLIENT_SECRET}" \
  --data-urlencode "audience=${OCCTOO_TENANT_ID}"
```

Prefer an explicit scope for integrations that need only one capability. This limits what a leaked token can do without changing the Application's broader configuration.

## Use the token

Send the access token as a bearer token with every protected API request:

```bash theme={null}
curl --request GET \
  --url 'https://api.occtoo.com/v1/events' \
  --header "Authorization: Bearer ${OCCTOO_ACCESS_TOKEN}"
```

Treat the token like a secret. Do not log it or persist it longer than necessary.

## Troubleshooting

* **`unauthorized_client`**: The Application is not authorized for the requested audience.
* **`invalid_scope`**: The scope is not enabled for the Application, or it does not belong to the requested audience.
* **`401 Unauthorized` from an API**: The token is missing, expired, malformed, or intended for another audience.
* **`403 Forbidden` from an API**: The token is valid, but the Application lacks the required scope or access to the requested resource.
