Credentials in managed agents

Credentials are server-managed secrets that let your agents reach third-party services without the secret ever entering the agent's environment. You store a credential once, reference it by ID, and the egress proxy resolves and injects it at request time.

Secret values are write-only. Once stored, they are never returned by any endpoint, so a compromised agent cannot read back the tokens it is using.

The primary place you use a credential is the network allowlist on environment.network. Store the secret first:

Python

from google import genai

client = genai.Client()

credential = client.credentials.create(
    id="github-production",
    type="bearer_token",
    token="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
)

print(f"Credential ID: {credential.id}, Status: {credential.status}")

JavaScript

import { GoogleGenAI } from "@google/genai";

const client = new GoogleGenAI({});

const credential = await client.credentials.create({
    id: "github-production",
    type: "bearer_token",
    token: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
});

console.log(`Credential ID: ${credential.id}, Status: ${credential.status}`);

REST

curl -X POST "https://generativelanguage.googleapis.com/v1beta/credentials" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "id": "github-production",
    "type": "bearer_token",
    "token": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}'

Then attach it to the domain it authenticates:

Python

interaction = client.interactions.create(
    agent="antigravity-preview-09-2026",
    input="Triage the open issues in my-org/my-repo.",
    environment={
        "type": "remote",
        "network": {
            "allowlist": [
                {"domain": "api.github.com", "credential": "github-production"},
                {"domain": "*"},
            ]
        },
    },
)

JavaScript

const interaction = await client.interactions.create({
    agent: "antigravity-preview-09-2026",
    input: "Triage the open issues in my-org/my-repo.",
    environment: {
        type: "remote",
        network: {
            allowlist: [
                { domain: "api.github.com", credential: "github-production" },
                { domain: "*" },
            ],
        },
    },
});

REST

curl -X POST "https://generativelanguage.googleapis.com/v1beta/interactions" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "agent": "antigravity-preview-09-2026",
    "input": "Triage the open issues in my-org/my-repo.",
    "environment": {
        "type": "remote",
        "network": {
            "allowlist": [
                { "domain": "api.github.com", "credential": "github-production" },
                { "domain": "*" }
            ]
        }
    }
}'

The agent now makes authenticated requests to api.github.com, and the token never exists inside the sandbox.

Credential types

Every credential has a type that determines which fields it accepts and how the proxy applies it.

Type Use case Behavior
bearer_token Personal access tokens, bot tokens, static API keys The proxy injects the token as a request header. No refresh logic.
oauth2 OAuth apps and user-delegated flows The proxy exchanges the refresh token for access tokens and refreshes them as they expire.
environment_variable Client SDKs that read secrets from the process environment The agent's environment receives a placeholder. The proxy substitutes the real secret on outbound requests.

Use credentials in the network allowlist

Add credential to an allowlist rule and the proxy authenticates every outbound request to that domain. This is the recommended way to give an agent access to a private API, a private repository, or a private bucket.

You can mix authenticated and unauthenticated rules in the same allowlist:

Python

interaction = client.interactions.create(
    agent="antigravity-preview-09-2026",
    input="Sync the open Jira issues into the tracking sheet in my repo.",
    environment={
        "type": "remote",
        "sources": [
            {
                "type": "repository",
                "source": "https://github.com/your-org/backend",
                "target": "/backend-app",
            }
        ],
        "network": {
            "allowlist": [
                {"domain": "github.com", "credential": "github-production"},
                {"domain": "api.atlassian.com", "credential": "jira-oauth"},
                {"domain": "*.googleapis.com"},
            ]
        },
    },
)

JavaScript

const interaction = await client.interactions.create({
    agent: "antigravity-preview-09-2026",
    input: "Sync the open Jira issues into the tracking sheet in my repo.",
    environment: {
        type: "remote",
        sources: [
            {
                type: "repository",
                source: "https://github.com/your-org/backend",
                target: "/backend-app",
            },
        ],
        network: {
            allowlist: [
                { domain: "github.com", credential: "github-production" },
                { domain: "api.atlassian.com", credential: "jira-oauth" },
                { domain: "*.googleapis.com" },
            ],
        },
    },
});

REST

curl -X POST "https://generativelanguage.googleapis.com/v1beta/interactions" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "agent": "antigravity-preview-09-2026",
    "input": "Sync the open Jira issues into the tracking sheet in my repo.",
    "environment": {
        "type": "remote",
        "sources": [
            {
                "type": "repository",
                "source": "https://github.com/your-org/backend",
                "target": "/backend-app"
            }
        ],
        "network": {
            "allowlist": [
                { "domain": "github.com", "credential": "github-production" },
                { "domain": "api.atlassian.com", "credential": "jira-oauth" },
                { "domain": "*.googleapis.com" }
            ]
        }
    }
}'

Because the proxy resolves the credential per request, an oauth2 credential refreshes its access token transparently. A long-running interaction does not break when the access token expires.

Combining credential and transform

Allowlist rules also accept an inline transform object that sets headers directly on the rule. Both mechanisms are applied by the egress proxy on the wire, so in both cases the header value never exists inside the sandbox. Both fields can appear on the same rule.

Rule configuration Behavior
credential only The proxy resolves the credential and injects its header on every request to the domain.
transform only Static header injection. The headers you write are sent as-is.
Both The credential is applied first, then transform merges on top. An explicit transform header wins if both set the same key.
Neither The domain is allowed and no headers are injected.

A credential is worth using when you want to store a secret once and reference it from every environment, agent, and trigger in your project, and when you want access token refresh and rotation handled for you. An inline transform fits when the value belongs to a single call, for example a token you generate yourself right before creating the interaction.

Combining the two is common. The credential carries the authentication header and transform adds whatever else the upstream service expects on the same request:

{
    "domain": "api.atlassian.com",
    "credential": "jira-oauth",
    "transform": {
        "X-Atlassian-Workspace": "my-workspace-id"
    }
}

To move a secret out of an inline transform and into a credential, store it with POST /credentials, replace the auth header in transform with "credential": "<id>", and leave the rest of the transform object alone.

Use credentials with MCP servers

Remote MCP servers take the same credential field. Set it on an mcp_server tool and the proxy injects the auth header into every request to that server:

Python

interaction = client.interactions.create(
    agent="antigravity-preview-09-2026",
    input="Create a new issue in my-org/my-repo",
    environment="remote",
    tools=[{
        "type": "mcp_server",
        "name": "github",
        "url": "https://api.githubcopilot.com/mcp",
        "credential": "github-production",
    }],
)

JavaScript

const interaction = await client.interactions.create({
    agent: "antigravity-preview-09-2026",
    input: "Create a new issue in my-org/my-repo",
    environment: "remote",
    tools: [{
        type: "mcp_server",
        name: "github",
        url: "https://api.githubcopilot.com/mcp",
        credential: "github-production",
    }],
});

REST

curl -X POST "https://generativelanguage.googleapis.com/v1beta/interactions" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "agent": "antigravity-preview-09-2026",
    "input": "Create a new issue in my-org/my-repo",
    "environment": "remote",
    "tools": [
        {
            "type": "mcp_server",
            "name": "github",
            "url": "https://api.githubcopilot.com/mcp",
            "credential": "github-production"
        }
    ]
}'

credential and headers follow the same precedence rule as the allowlist. The credential is applied first and headers merges on top, so an explicit header wins if both set the same key:

{
    "type": "mcp_server",
    "name": "jira",
    "url": "https://jira.atlassian.com/mcp",
    "credential": "jira-oauth",
    "headers": {
        "X-Atlassian-Workspace": "my-workspace-id"
    }
}

To move a secret out of inline headers and into a credential, store it with POST /credentials and replace the auth entry in headers with credential. Keep the other headers where they are.

Use credentials as environment variables

Some client libraries read secrets from the process environment rather than accepting them as request headers. Socket-mode and long-polling clients are the common case.

Bind an environment_variable credential to a variable name under environment.env:

Python

interaction = client.interactions.create(
    agent="antigravity-preview-09-2026",
    input="Run the sync script and check notifications.",
    environment={
        "type": "remote",
        "env": {
            "NODE_ENV": "production",
            "SLACK_BOT_TOKEN": {"credential": "slack-bot-token"},
        },
    },
)

JavaScript

const interaction = await client.interactions.create({
    agent: "antigravity-preview-09-2026",
    input: "Run the sync script and check notifications.",
    environment: {
        type: "remote",
        env: {
            NODE_ENV: "production",
            SLACK_BOT_TOKEN: { credential: "slack-bot-token" },
        },
    },
});

REST

curl -X POST "https://generativelanguage.googleapis.com/v1beta/interactions" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "agent": "antigravity-preview-09-2026",
    "input": "Run the sync script and check notifications.",
    "environment": {
        "type": "remote",
        "env": {
            "NODE_ENV": "production",
            "SLACK_BOT_TOKEN": { "credential": "slack-bot-token" }
        }
    }
}'

env accepts literal strings and credential references side by side. A literal string is injected into the container as a normal plaintext variable.

A credential reference is not. The variable receives the placeholder __GEMINI_CRED_<credential-id>__, and the proxy swaps in the real secret only for outbound requests going to a domain in the credential's trusted_domains. A request to any other domain is rejected, so the secret never leaves the perimeter and the placeholder is not sent in its place.

Set trusted_domains on every environment_variable credential. It is the control that scopes where the secret can be used.

Create a credential

Every create request needs a type, plus whichever fields that type requires.

When calling REST directly, all field names use snake_case. Sending a camelCase field returns a 400.

Bearer token

A bearer token credential needs only token:

Python

credential = client.credentials.create(
    id="github-production",
    type="bearer_token",
    token="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
)

JavaScript

const credential = await client.credentials.create({
    id: "github-production",
    type: "bearer_token",
    token: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
});

REST

curl -X POST "https://generativelanguage.googleapis.com/v1beta/credentials" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "id": "github-production",
    "type": "bearer_token",
    "token": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}'

The response returns metadata only, never the token:

{
  "id": "github-production",
  "type": "bearer_token",
  "status": "active",
  "create_time": "2026-07-15T10:00:00.000000000Z",
  "update_time": "2026-07-15T10:00:00.000000000Z"
}

By default the proxy sends Authorization: Bearer <token>. Override header_name and prefix to target a service that expects something else:

Python

credential = client.credentials.create(
    id="my-api-key",
    type="bearer_token",
    token="key_xxxxxxxxxxxx",
    header_name="x-goog-api-key",
    prefix="",
)

JavaScript

const credential = await client.credentials.create({
    id: "my-api-key",
    type: "bearer_token",
    token: "key_xxxxxxxxxxxx",
    header_name: "x-goog-api-key",
    prefix: "",
});

REST

curl -X POST "https://generativelanguage.googleapis.com/v1beta/credentials" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "id": "my-api-key",
    "type": "bearer_token",
    "token": "key_xxxxxxxxxxxx",
    "header_name": "x-goog-api-key",
    "prefix": ""
}'

This configuration produces the header x-goog-api-key: key_xxxxxxxxxxxx.

The following table shows how header_name and prefix combine:

Configuration Injected header
{"token": "ghp_xxx"} Authorization: Bearer ghp_xxx
{"token": "sk_live_xxx"} Authorization: Bearer sk_live_xxx
{"token": "key_xxx", "header_name": "x-goog-api-key", "prefix": ""} x-goog-api-key: key_xxx
{"token": "mytoken", "header_name": "X-API-Token", "prefix": ""} X-API-Token: mytoken

OAuth2

An OAuth2 credential requires client_id, client_secret, refresh_token, and token_url. The scopes field is optional:

Python

credential = client.credentials.create(
    id="jira-oauth",
    type="oauth2",
    client_id="my-client-id",
    client_secret="my-client-secret",
    token_url="https://auth.atlassian.com/oauth/token",
    refresh_token="rt_xxxxxxxxxxxxxxxxxxxx",
    scopes=["read:jira-work", "write:jira-work"],
)

JavaScript

const credential = await client.credentials.create({
    id: "jira-oauth",
    type: "oauth2",
    client_id: "my-client-id",
    client_secret: "my-client-secret",
    token_url: "https://auth.atlassian.com/oauth/token",
    refresh_token: "rt_xxxxxxxxxxxxxxxxxxxx",
    scopes: ["read:jira-work", "write:jira-work"],
});

REST

curl -X POST "https://generativelanguage.googleapis.com/v1beta/credentials" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "id": "jira-oauth",
    "type": "oauth2",
    "client_id": "my-client-id",
    "client_secret": "my-client-secret",
    "token_url": "https://auth.atlassian.com/oauth/token",
    "refresh_token": "rt_xxxxxxxxxxxxxxxxxxxx",
    "scopes": ["read:jira-work", "write:jira-work"]
}'

Creating an OAuth2 credential performs a live token exchange against token_url to confirm the configuration works. The credential is stored only if the provider returns a successful token response containing an access_token. Both JSON and form-urlencoded responses are accepted.

This means you need a valid, unexpired refresh token at creation time. If the provider rejects the exchange, the error is returned to you:

{
  "error": {
    "message": "OAuth token validation failed with HTTP 403: {\"error\":\"unauthorized_client\",\"error_description\":\"refresh_token is invalid\"}",
    "code": "invalid_request"
  }
}

Once stored, the proxy refreshes access tokens as they expire. If the provider rotates refresh tokens and returns a new one during a refresh, the new token replaces the stored one automatically.

Environment variable

An environment_variable credential requires value and injection_location:

Python

credential = client.credentials.create(
    id="slack-bot-token",
    type="environment_variable",
    value="xoxb-xxxxxxxxxxxx-xxxxxxxxxxxx",
    trusted_domains=["*.slack.com", "slack.com"],
    injection_location="header",
)

JavaScript

const credential = await client.credentials.create({
    id: "slack-bot-token",
    type: "environment_variable",
    value: "xoxb-xxxxxxxxxxxx-xxxxxxxxxxxx",
    trusted_domains: ["*.slack.com", "slack.com"],
    injection_location: "header",
});

REST

curl -X POST "https://generativelanguage.googleapis.com/v1beta/credentials" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "id": "slack-bot-token",
    "type": "environment_variable",
    "value": "xoxb-xxxxxxxxxxxx-xxxxxxxxxxxx",
    "trusted_domains": ["*.slack.com", "slack.com"],
    "injection_location": "header"
}'

The injection_location field tells the proxy where in the outbound request to substitute the secret. It accepts header, query, or body, either as a single string or as an array when a service needs more than one:

"injection_location": ["header", "query"]

Substitution happens only in the locations you list. A request that carries the placeholder anywhere else is rejected rather than sent on.

To bind the credential to a variable name, see Use credentials as environment variables.

Generated IDs

The id field is optional. Omit it and the service generates a UUID:

{
  "id": "9e545973-4330-49bb-9a44-930cea9fbe3c",
  "type": "bearer_token",
  "status": "active",
  "create_time": "2026-07-15T10:00:00.000000000Z",
  "update_time": "2026-07-15T10:00:00.000000000Z"
}

Supply your own ID when you want a stable, readable reference to use across interactions. Since the ID appears in the resource path, prefer lowercase alphanumerics with hyphens or underscores.

List credentials

List the credentials belonging to your project. Use pagination parameters to control the response batch size.

Python

response = client.credentials.list(page_size=10)
for credential in response.credentials:
    print(f"Credential ID: {credential.id}, Type: {credential.type}")

JavaScript

const response = await client.credentials.list({ page_size: 10 });
for (const credential of response.credentials) {
    console.log(`Credential ID: ${credential.id}, Type: ${credential.type}`);
}

REST

curl -X GET "https://generativelanguage.googleapis.com/v1beta/credentials?page_size=10" \
-H "x-goog-api-key: $GEMINI_API_KEY"

The response contains metadata only:

{
  "credentials": [
    {
      "id": "github-production",
      "type": "bearer_token",
      "status": "active",
      "create_time": "2026-07-15T10:00:00.000000000Z",
      "update_time": "2026-07-15T10:00:00.000000000Z"
    },
    {
      "id": "jira-oauth",
      "type": "oauth2",
      "status": "active",
      "create_time": "2026-07-15T10:05:00.000000000Z",
      "update_time": "2026-07-15T10:05:00.000000000Z"
    }
  ],
  "next_page_token": "Cj...5aE="
}

Pass next_page_token back as page_token to fetch the next page. The field is omitted when there are no further results.

Parameter Type Description
page_size integer Maximum number of credentials per page.
page_token string Token from the next_page_token of a previous response.

Get a credential

Retrieve metadata for a specific credential by its ID.

Python

credential = client.credentials.get(id="github-production")
print(f"Credential ID: {credential.id}, Status: {credential.status}")

JavaScript

const credential = await client.credentials.get("github-production");
console.log(`Credential ID: ${credential.id}, Status: ${credential.status}`);

REST

curl -X GET "https://generativelanguage.googleapis.com/v1beta/credentials/github-production" \
-H "x-goog-api-key: $GEMINI_API_KEY"

The response looks similar to the following:

{
  "id": "github-production",
  "type": "bearer_token",
  "status": "active",
  "create_time": "2026-07-15T10:00:00.000000000Z",
  "update_time": "2026-08-01T14:30:00.000000000Z"
}

Requesting a credential that does not exist returns 404:

{
  "error": {
    "message": "Result not found.; GetCredential call failed",
    "code": "not_found"
  }
}

Rotate a credential

Replace a secret without touching any allowlist rule, tool definition, or environment variable that references it. Rotation takes effect on the next proxy resolution.

The request must include type, plus the fields you want to change. Fields you omit keep their current values.

Rotate a bearer token:

Python

credential = client.credentials.update(
    id="github-production",
    type="bearer_token",
    token="ghp_new_xxxxxxxxxxxxxxxxxxxx",
)

JavaScript

const credential = await client.credentials.update("github-production", {
    type: "bearer_token",
    token: "ghp_new_xxxxxxxxxxxxxxxxxxxx",
});

REST

curl -X PATCH "https://generativelanguage.googleapis.com/v1beta/credentials/github-production" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "type": "bearer_token",
    "token": "ghp_new_xxxxxxxxxxxxxxxxxxxx"
}'

Rotate an OAuth2 refresh token:

Python

credential = client.credentials.update(
    id="jira-oauth",
    type="oauth2",
    refresh_token="rt_new_xxxxxxxxxxxxxxxxxxxx",
)

JavaScript

const credential = await client.credentials.update("jira-oauth", {
    type: "oauth2",
    refresh_token: "rt_new_xxxxxxxxxxxxxxxxxxxx",
});

REST

curl -X PATCH "https://generativelanguage.googleapis.com/v1beta/credentials/jira-oauth" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-d '{
    "type": "oauth2",
    "refresh_token": "rt_new_xxxxxxxxxxxxxxxxxxxx"
}'

The response reflects the new update_time:

{
  "id": "jira-oauth",
  "type": "oauth2",
  "status": "active",
  "create_time": "2026-07-15T10:05:00.000000000Z",
  "update_time": "2026-08-01T14:30:00.000000000Z"
}

A credential's type is fixed at creation. To change it, delete the credential and create a new one.

Delete a credential

Delete a credential and its stored secret when it is no longer needed.

Python

client.credentials.delete(id="github-production")

JavaScript

await client.credentials.delete("github-production");

REST

curl -X DELETE "https://generativelanguage.googleapis.com/v1beta/credentials/github-production" \
-H "x-goog-api-key: $GEMINI_API_KEY"

A successful delete returns an empty object:

{}

Any allowlist rule, tool, or environment variable still referencing the ID will fail to resolve, so update those first.

Field reference

Fields common to every credential:

Field Type Required Description
id string No Unique identifier. Generated as a UUID when omitted.
type string Yes One of bearer_token, oauth2, environment_variable.
status string Read-only Current status of the credential.
create_time string Read-only RFC 3339 creation timestamp.
update_time string Read-only RFC 3339 timestamp of the last update.

Fields for bearer_token:

Field Type Required Description
token string Yes Write-only. The token value.
header_name string No Header to inject. Defaults to Authorization.
prefix string No Value prefix. Defaults to Bearer. Set to "" for none.

Fields for oauth2:

Field Type Required Description
client_id string Yes OAuth2 client ID.
client_secret string Yes Write-only. OAuth2 client secret.
refresh_token string Yes Write-only. Refresh token used to obtain access tokens.
token_url string Yes Provider token endpoint.
scopes array No OAuth scopes to request.

Fields for environment_variable:

Field Type Required Description
value string Yes Write-only. The secret value.
injection_location string or array Yes Where to substitute the secret. One or more of header, query, body.
trusted_domains array No Domain patterns authorized for substitution.

Errors

Errors return a JSON object with a message and a code:

{
  "error": {
    "message": "Credential 'github-production' already exists.; CreateCredential call failed",
    "code": "aborted"
  }
}
HTTP status code Cause
400 invalid_request Missing required field, unknown field, unsupported type, or a failed OAuth2 validation.
404 not_found No credential with that ID.
409 aborted A credential with that ID already exists.

Unknown fields are rejected rather than ignored, and the error names the field:

{
  "error": {
    "message": "Unknown parameter 'headerName'. Did you mean 'header_name'?",
    "code": "invalid_request"
  }
}

What's next