Add SDK to Your App
In 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
Section titled “Prerequisites”- Complete First Protected Call.
- Keep Caracal running.
- Install one supported runtime from the table below.
Write the Configuration Profile
Section titled “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:
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_idandapplication_ididentify your program: it acts as the Anton application inside your zone.app_client_secret_filepoints 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.
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:
export CARACAL_CONFIG=/path/to/caracal.tomlexport CARACAL_RESOURCE_ID=resource://openaiexport CARACAL_RESOURCE_PATH=/v1/modelsexport CARACAL_RESOURCE_SCOPE=openai:models$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"Install the SDK
Section titled “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
Section titled “Make the Call”import { Caracal } from '@caracalai/sdk'
const caracal = new Caracal()const resourceId = process.env.CARACAL_RESOURCE_IDconst resourcePath = process.env.CARACAL_RESOURCE_PATHconst 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()}import asyncioimport 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())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))}Expected Outcome
Section titled “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
Section titled “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 byCARACAL_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.gatewayRequest()/gateway_request()/GatewayRequest()builds the Gateway URL and theX-Caracal-Resourcerouting 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, Python, 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
Section titled “Common Mistakes”- Use the resource identifier (
resource://openai), not the upstream URL, as the[[credentials]]entry key. - Give
gatewayRequesta 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
Section titled “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. Otherwise you have completed Get Started: continue to Tutorials to protect a real API, or read Concepts for the full model behind what you just built.

