---
title: "Add SDK to Your App"
url: "https://docs.caracal.run/v1.0/get-started/add-sdk-to-your-app/"
markdown_url: "https://docs.caracal.run/markdown/v1.0/get-started/add-sdk-to-your-app.md"
description: "Turn the throwaway script from First Protected Call into an application integration with a durable configuration profile."
page_type: "workflow"
concepts: []
requires: []
---

# Add SDK to Your App

Canonical URL: https://docs.caracal.run/v1.0/get-started/add-sdk-to-your-app/
Markdown URL: https://docs.caracal.run/markdown/v1.0/get-started/add-sdk-to-your-app.md
Description: Turn the throwaway script from First Protected Call into an application integration with a durable configuration profile.
Page type: workflow
Concepts: none
Requires: none

---

import { Tabs, TabItem } from '@astrojs/starlight/components'

In [First Protected Call](/v1.0/get-started/first-protected-call/) a throwaway script proved the enforcement path with three environment variables. This page turns that into the shape real applications use: a durable **configuration profile** instead of pasted variables, and an example parameterized so the same code reaches any resource you protect. Pick one language; the three examples are equivalent.

## Prerequisites

* Complete [First Protected Call](/v1.0/get-started/first-protected-call/).
* Keep Caracal running.
* Install one supported runtime from the table below.

## Write the Configuration Profile

Inline environment variables work for a script; an application needs configuration it can deploy. The SDK reads a small TOML file called a profile, named by the `CARACAL_CONFIG` environment variable. You write this file yourself, using the values from First Protected Call - the zone ID, the application ID, and the application client secret you copied during guided setup:

```toml
zone_id = "<Pied Piper Production zone ID>"
application_id = "<Anton application ID>"
app_client_secret_file = "/path/to/anton-client-secret"

[[credentials]]
resource = "resource://openai"
upstream_prefix = "https://api.openai.com"
```

Reading it line by line:

* `zone_id` and `application_id` identify your program: it acts as the Anton application inside your zone.
* `app_client_secret_file` points to a file containing the client secret, readable only by your user. The SDK never searches for credentials on its own; the path must be explicit. If you lost the secret, reveal it again from the application's detail panel in the console (each reveal is audited).
* The `[[credentials]]` entry declares which resource this program uses. Its key is the resource identifier, not the upstream URL.

On the local stack the SDK's built-in defaults already point at the right service URLs, so this is the whole file. For cloud or custom deployments you would add `sts_url`, `coordinator_url`, and `gateway_url`; the full schema is in [Configure Workloads](/v1.0/runtime-console/config-file/).

:::note[Key takeaway]
One profile serves your whole service. It names the application your program acts as - not each agent inside it. Every run executes under this one application identity, so you do not register an application per agent.
:::

Point the examples at the profile and choose what they call. When `CARACAL_CONFIG` is set, the SDK reads exactly that profile - the identity variables from the previous page are no longer needed:

<Tabs syncKey="os">
  <TabItem label="Linux / macOS">
    ```sh
    export CARACAL_CONFIG=/path/to/caracal.toml
    export CARACAL_RESOURCE_ID=resource://openai
    export CARACAL_RESOURCE_PATH=/v1/models
    export CARACAL_RESOURCE_SCOPE=openai:models
    ```
  </TabItem>

  <TabItem label="Windows">
    ```powershell
    $env:CARACAL_CONFIG = "C:\path\to\caracal.toml"
    $env:CARACAL_RESOURCE_ID = "resource://openai"
    $env:CARACAL_RESOURCE_PATH = "/v1/models"
    $env:CARACAL_RESOURCE_SCOPE = "openai:models"
    ```
  </TabItem>
</Tabs>

## Install the SDK

Install the package for your application language.

| Language   | Install command                                           | Runtime requirement |
| ---------- | --------------------------------------------------------- | ------------------- |
| TypeScript | `npm install @caracalai/sdk` or `pnpm add @caracalai/sdk` | Node.js 22+         |
| Python     | `pip install caracalai-sdk`                               | Python 3.12+        |
| Go         | `go get github.com/garudex-labs/caracal/packages/sdk/go`  | Go 1.26+            |

## Make the Call

<Tabs syncKey="lang">
  <TabItem label="TypeScript">
    ```typescript
    import { Caracal } from '@caracalai/sdk'

    const caracal = new Caracal()
    const resourceId = process.env.CARACAL_RESOURCE_ID
    const resourcePath = process.env.CARACAL_RESOURCE_PATH
    const resourceScope = process.env.CARACAL_RESOURCE_SCOPE

    if (!resourceId) throw new Error('CARACAL_RESOURCE_ID is required')
    if (!resourcePath) throw new Error('CARACAL_RESOURCE_PATH is required')
    if (!resourceScope) throw new Error('CARACAL_RESOURCE_SCOPE is required')

    const governedFetch = caracal.applicationTransport(resourceId, {
        scopes: [resourceScope],
    })
    const target = caracal.gatewayRequest(resourceId, resourcePath)
    try {
        const response = await governedFetch(target.url, {
            method: 'GET',
        })
        if (!response.ok) throw new Error(`protected call failed: ${response.status}`)
        console.log(await response.text())
    } finally {
        await caracal.close()
    }
    ```
  </TabItem>

  <TabItem label="Python">
    ```python
    import asyncio
    import os

    from caracalai import Caracal

    caracal = Caracal()
    resource_id = os.environ["CARACAL_RESOURCE_ID"]
    resource_path = os.environ["CARACAL_RESOURCE_PATH"]
    resource_scope = os.environ["CARACAL_RESOURCE_SCOPE"]

    async def main():
        target = caracal.gateway_request(resource_id, resource_path)
        try:
            async with caracal.application_transport(
                resource_id,
                scopes=[resource_scope],
            ) as governed:
                response = await governed.get(target.url)
                response.raise_for_status()
                print(response.text)
        finally:
            await caracal.aclose()

    asyncio.run(main())
    ```
  </TabItem>

  <TabItem label="Go">
    ```go
    package main

    import (
        "fmt"
        "io"
        "os"

        caracal "github.com/garudex-labs/caracal/packages/sdk/go"
    )

    func main() {
        c, err := caracal.New()
        if err != nil {
            panic(err)
        }

        resourceID := os.Getenv("CARACAL_RESOURCE_ID")
        resourcePath := os.Getenv("CARACAL_RESOURCE_PATH")
        resourceScope := os.Getenv("CARACAL_RESOURCE_SCOPE")
        if resourceID == "" || resourcePath == "" || resourceScope == "" {
            panic("CARACAL_RESOURCE_ID, CARACAL_RESOURCE_PATH, and CARACAL_RESOURCE_SCOPE are required")
        }
        defer c.Close()

        // nil base: the SDK constructs its own HTTP client.
        governed, err := c.ApplicationTransport(nil, resourceID, caracal.ApplicationTransportOptions{
            Scopes: []string{resourceScope},
        })
        if err != nil {
            panic(err)
        }
        target, err := c.GatewayRequest(resourceID, resourcePath)
        if err != nil {
            panic(err)
        }
        resp, err := governed.Get(target.URL)
        if err != nil {
            panic(err)
        }
        defer resp.Body.Close()
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("protected call failed: %s", resp.Status))
        }
        body, _ := io.ReadAll(resp.Body)
        fmt.Println(string(body))
    }
    ```
  </TabItem>
</Tabs>

## Expected Outcome

Run the example in your selected language. It should print the provider's JSON model list - the same response your first script returned - and create matching authorization and Gateway action-result events in **Audit**. A constructor or profile error means no request reached Caracal; a policy denial appears in Audit with a request ID.

## What Each Call Does

All three examples follow the same four calls; only the naming convention changes per language:

* `new Caracal()` / `Caracal()` / `New()` loads the profile named by `CARACAL_CONFIG` (or, without one, `CARACAL_*` environment variables, as in First Protected Call). It fails immediately if the configuration is missing or unreadable.
* `applicationTransport()` / `application_transport()` / `ApplicationTransport()` returns an HTTP transport pinned to one resource. Each call asks Caracal's token service (the **STS**, the component that evaluates policy and issues mandates) for authority with exactly the scopes you list and gets a fresh, short-lived, replay-protected mandate for the request. To do this it opens a **session** - Caracal's record of one governed run of your program - and narrows the requested scope through a **delegation**, a hand-off that can only ever shrink permissions, never grow them. The SDK manages both for you; you will meet them properly in [Concepts](/v1.0/concepts/).
* `gatewayRequest()` / `gateway_request()` / `GatewayRequest()` builds the Gateway URL and the `X-Caracal-Resource` routing header that names the resource.
* `close()` / `aclose()` / `Close()` ends SDK-owned sessions and background work cleanly.

`applicationTransport()` is the smallest application-owned call path and the right starting point. Agent frameworks that already run under delegated authority use `session()` with a scoped transport instead; read your language's SDK guide ([TypeScript](/v1.0/guides/sdk-typescript/), [Python](/v1.0/guides/sdk-python/), [Go](/v1.0/guides/sdk-go/)) before switching paths.

If policy denies the exchange, the SDK surfaces the error from the STS. Open **Audit** in the web console with the request ID to see the determining policy and diagnostics.

## Common Mistakes

* Use the resource identifier (`resource://openai`), not the upstream URL, as the `[[credentials]]` entry key.
* Give `gatewayRequest` a relative path; it rejects absolute URLs and dot segments.
* Request at least one scope, and keep every scope within what the active policy allows.
* Close the client on shutdown so sessions and background work terminate cleanly.

## Next Step

If anything failed - the profile would not load, the exchange was denied, the Gateway refused the call, or audit shows nothing - walk [First-Run Troubleshooting](/v1.0/get-started/first-run-troubleshooting/). Otherwise you have completed Get Started: continue to [Tutorials](/v1.0/tutorials/) to protect a real API, or read [Concepts](/v1.0/concepts/) for the full model behind what you just built.
