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

# Credential grants

> Call an external API with a user credential that your code cannot read

## What is a credential grant?

A credential grant is a named credential that belongs to one user. It has two parts:

* **Secret**: the token, key, or password
* **Policy**: the destinations that can receive the secret and the code that can use the grant

Your code calls `fetch(grant, input, init)` with the grant name. Standard Agents Builder adds the secret to the outgoing request and returns the response. Your code, the model, and the client receive no copy of the secret.

Use a grant for a credential that authenticates one user to an external HTTPS API, such as that user's GitHub token.

## Creating a grant

An administrator creates or replaces a grant with one request:

```
PUT /api/users/:id/grants/:name
```

```bash theme={null}
curl -X PUT "https://your-instance.example.com/api/users/$USER_ID/grants/github" \
  -H "Authorization: Bearer $STANDARD_AGENTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "bearer",
    "secret": "ghp_example",
    "origins": ["https://api.github.com"],
    "allow": {
      "userEndpoints": ["github/repos"],
      "agents": ["repo_agent"]
    },
    "metadata": { "account": "octocat" }
  }'
```

A successful request returns HTTP 204 with an empty body. A second `PUT` to the same name replaces the secret and the policy.

### Request body

| Field | Required | Description |
| - | - | - |
| `kind` | Yes | `bearer`, `header`, `query`, or `basic`. See [Kinds and injection](#kinds-and-injection) |
| `secret` | Yes | The credential. A string of 1 to 16,384 characters. A `bearer` or `header` secret cannot contain a line break |
| `origins` | Yes | 1 to 64 destinations that can receive the secret. See [Origin matching](#origin-matching) |
| `allow` | Yes | The callers that can use the grant. See [Allow policy](#allow-policy) |
| `inject` | No | Where the secret goes in the request: `header`, `format`, and `query` |
| `metadata` | No | A JSON object of non-secret data, such as an account label. At most 16,384 characters as JSON |
| `name` | No | When present, it must equal the name in the path |

A grant name starts with a lowercase letter or a digit. The remaining characters are lowercase letters, digits, `.`, `_`, and `-`. The maximum length is 128 characters.

The request returns HTTP 400 with `{ "error": "Invalid credential grant" }` when a field fails these rules or when the body has a field that this table does not list. It returns HTTP 404 when the user does not exist and HTTP 403 when the caller is not an administrator.

## Listing grants

```
GET /api/users/:id/grants
```

The response lists the policy of each grant, sorted by name. It contains no secret:

```json theme={null}
{
  "grants": [
    {
      "name": "github",
      "kind": "bearer",
      "origins": ["https://api.github.com"],
      "allow": {
        "userEndpoints": ["github/repos"],
        "agents": ["repo_agent"]
      },
      "metadata": { "account": "octocat" }
    }
  ]
}
```

The list also contains the [instance grants](#instance-grants) that apply to the user.

## Deleting a grant

```
DELETE /api/users/:id/grants/:name
```

The request returns HTTP 204, including when the user has no grant with that name. It deletes the user's own grant only.

* When no [instance grant](#instance-grants) has that name, `fetch()` with that grant name returns the `grant_not_found` error after the deletion.
* When an instance grant has that name, it applies to the user again after the deletion. `fetch()` then uses the secret and the policy of the instance grant.

To revoke a credential that an instance grant supplies, remove the grant from the instance registry or change its `allow` policy.

## The secret is write-only

The `PUT` request is the one operation that accepts a secret. No route returns it, and `UserState` has no member that reads it. To change a secret, send a new `PUT` request with the full grant.

Standard Agents Builder stores each grant in the durable storage of the user it belongs to. The policy is stored next to the secret, and the runtime reads the secret only when it adds the credential to a `fetch()` request.

The three grant routes accept administrators only. The policy, including `metadata`, is readable through `GET /api/users/:id/grants`. Code in `agents/` cannot list grants or read their policy.

## Kinds and injection

`kind` sets how the runtime adds the secret to the request. `inject` adjusts the location for the `header` and `query` kinds.

| Kind | Injection | `inject` fields |
| - | - | - |
| `bearer` | Sets `Authorization: Bearer <secret>` | None |
| `header` | Sets one header to the secret | `header` names the header. The default is `Authorization`. `format` is the header value, and the runtime replaces each `SECRET` in it with the secret. The default is `SECRET` |
| `query` | Sets one query parameter to the secret | `query` names the parameter. The default is `key` |
| `basic` | Sets `Authorization: Basic <base64 of the secret>` | None |

For the `basic` kind, store the secret as `username:password`.

A `header` grant for an API that expects `X-Api-Key: <secret>`:

```json theme={null}
{
  "kind": "header",
  "secret": "sk_example",
  "origins": ["https://api.example.com"],
  "inject": { "header": "X-Api-Key" },
  "allow": { "agents": ["support_agent"] }
}
```

A `header` grant for an API that expects `Authorization: token <secret>`:

```json theme={null}
{
  "kind": "header",
  "secret": "ghp_example",
  "origins": ["https://api.github.com"],
  "inject": { "format": "token SECRET" },
  "allow": { "agents": ["repo_agent"] }
}
```

The injected header or query parameter replaces a value with the same name that the caller supplied.

Rules for `inject`:

* `header` is a valid HTTP header name.
* `format` has at most 1,024 characters and no line break.
* `query` has 1 to 128 characters from letters, digits, `.`, `_`, `~`, and `-`.

Standard Agents Builder does not support the `signing` kind. A `PUT` request with `"kind": "signing"` returns HTTP 400.

## Origin matching

The runtime sends the secret only to a destination that an `origins` entry covers. An entry has one of two forms:

| Form | Example | Matches |
| - | - | - |
| Exact origin | `https://api.github.com` | A URL with the same scheme, host, and port |
| Suffix wildcard | `*.example.com` | An `https` URL whose host name ends with `.example.com` |

* An exact origin uses `https`. Write it in lowercase with no path, no trailing slash, and no user name or password.
* A wildcard entry does not match the bare domain. `*.example.com` matches `api.example.com` and does not match `example.com`. Add `https://example.com` as a second entry to cover it.
* Each entry has at most 512 characters.

The runtime checks the origin before it checks the caller.

### Redirects

The runtime follows redirects itself and checks each redirect destination against `origins`.

* A redirect to a destination that no `origins` entry covers ends the fetch with the `grant_origin_denied` error. The runtime sends no request to that destination.
* The runtime adds the credential again to each request that it sends, and it sends the headers from your code with each one.
* The runtime follows at most 5 redirects. A sixth redirect ends the fetch with the `grant_redirect_limit` error.
* A 303 response changes the method to `GET`, except for a `GET` or `HEAD` request. A 301 or 302 response does the same for a `POST` request. The runtime then removes the body and the headers that describe it, such as `Content-Type`. A 307 or 308 response keeps the method and the body.
* The runtime does not forward a `redirect` option from your code.

## Allow policy

`allow` names the code that can use the grant. Each field is a list of up to 128 names. A caller that no list names receives the `grant_caller_denied` error. A `PUT` request needs at least one list.

| Field | Caller | Name to list |
| - | - | - |
| `userEndpoints` | A user endpoint that calls `user.fetch()` | The endpoint path under `agents/users/`, such as `github/repos`. The name with a method suffix, such as `github/repos.post`, limits the entry to that file |
| `agents` | Thread code that calls `state.user()` and then `fetch()` | The name of the agent that the thread runs |

Thread code means tools, hooks, handlers, effects, and thread endpoints. Standard Agents Builder identifies all of them by the agent of the thread, so `allow.agents` is the list that admits them.

Two more callers use the `userEndpoints` list:

* A [user lifecycle hook](https://docs.standardagents.ai/core-concepts/users.md#user-lifecycle-hooks) matches its hook type, such as `user_value_changed`.
* A [user room](https://docs.standardagents.ai/core-concepts/users.md#user-rooms) matches the name in its `endpoint` option. A room without that option matches its path under `agents/users/`, such as `my_package/room`.

The `PUT` route also accepts `allow.tools` and `allow.endpoints` and stores them. No caller in Standard Agents Builder matches those two lists. The route rejects `allow.userRooms`.

## Using a grant

`fetch(grant, input, init)` takes the grant name, a URL, and request options. It is a member of `UserState` and of the handle that `state.user()` returns.

### From a user endpoint

```typescript theme={null}
// agents/users/github/repos.ts
import { defineUserEndpoint } from '@standardagents/spec';

export default defineUserEndpoint(async (_req, user) => {
  const response = await user.fetch('github', 'https://api.github.com/user/repos', {
    headers: { Accept: 'application/vnd.github+json' },
  });
  if (!response.ok) {
    return Response.json({ error: 'The GitHub request failed.' }, { status: 502 });
  }
  return Response.json(await response.json());
});
```

This endpoint needs `github/repos` in `allow.userEndpoints`.

### From a tool

```typescript theme={null}
// agents/tools/create_github_issue.ts
import { defineTool } from '@standardagents/spec';
import { z } from 'zod';

export default defineTool({
  description: 'Create an issue in one of the user\'s GitHub repositories.',
  args: z.object({
    repo: z.string().describe('The repository, as owner/name'),
    title: z.string().describe('The issue title'),
  }),
  execute: async (state, args) => {
    const user = await state.user();
    if (!user) {
      return { status: 'error', error: 'This thread has no user.' };
    }

    const response = await user.fetch('github', `https://api.github.com/repos/${args.repo}/issues`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title: args.title }),
    });
    if (!response.ok) {
      return { status: 'error', error: `GitHub returned HTTP ${response.status}.` };
    }
    return { status: 'success', result: await response.text() };
  },
});
```

This tool needs the thread's agent name in `allow.agents`.

### How the runtime handles the request

1. The runtime reads the user's grant with that name. When the user has none, it reads the [instance grant](#instance-grants) with that name.
2. It checks the destination against `origins`.
3. It checks the caller against `allow`.
4. It adds the secret to the request as `kind` and `inject` describe.
5. It sends the request with redirects disabled.
6. For a redirect response, it checks the new destination against `origins`, adds the secret again, and sends the next request. It repeats this step for at most 5 redirects.
7. It returns the final response of the external API to your code.

The runtime forwards the `method`, `headers`, and `body` options. The default method is `GET`. `body` is a string or absent, and `fetch()` throws for any other body type.

### Errors

When the runtime does not send the request, `fetch()` resolves with a JSON response that carries an error code:

| Status | Body `error` | Cause |
| - | - | - |
| 404 | `grant_not_found` | No grant with that name applies to the user. The body also has the `grant` name |
| 400 | `grant_target_invalid` | `input` is not a valid absolute URL |
| 403 | `grant_origin_denied` | No `origins` entry covers the destination, or the destination of a redirect |
| 403 | `grant_caller_denied` | No `allow` list names the caller |
| 500 | `grant_config_invalid` | The stored policy cannot be read |
| 501 | `grant_kind_unsupported` | The grant has a kind that the runtime cannot inject |
| 502 | `grant_redirect_limit` | The external API returned more than 5 redirects in a row |
| 502 | `grant_fetch_failed` | The request to the external API failed at the network level |

The external API can return the same status codes. Read the `error` field of the body to tell a grant error from an API response. The error bodies contain neither the secret nor the destination URL.

## Instance grants

An instance grant applies to every user of the instance. Set the secret `AGENT_BUILDER_CREDENTIAL_GRANTS` to a JSON registry:

```json theme={null}
{
  "version": 1,
  "grants": [
    {
      "name": "weather",
      "kind": "query",
      "secret": "wk_example",
      "origins": ["https://api.weather.example.com"],
      "inject": { "query": "appid" },
      "allow": { "agents": ["weather_agent"] }
    }
  ]
}
```

* Each entry follows the rules of the `PUT` request body, and `name` is required.
* The registry holds at most 64 grants and at most 65,536 characters.
* When the registry is malformed, or two entries share a name, the runtime uses no instance grants.
* A user's own grant takes precedence over an instance grant with the same name. After the user's grant is deleted, the instance grant applies again.

## Grant or variable

[`state.env(name)`](https://docs.standardagents.ai/core-concepts/tools) and `user.env(name)` return the value of a variable to your code. Code that holds a value can log it, return it to the model, or send it to any destination.

| Use | When |
| - | - |
| Credential grant | The credential authenticates one user to an external HTTPS API, and the API accepts it in a header, a query parameter, or Basic authentication |
| Thread or user variable | Your code needs the value itself: to pass it to an SDK, to compute a signature, to send a request body that is not a string, or to store configuration that is not a secret |

A grant also limits the credential to listed destinations and listed callers. A variable has no such limits.

## Next steps

> [card] [Users](https://docs.standardagents.ai/core-concepts/users.md)
>
> Run endpoints for one user and push live events to that user

> [card] [Tools](https://docs.standardagents.ai/core-concepts/tools.md)
>
> Read variables with `state.env()`

> [card] [API](https://docs.standardagents.ai/core-concepts/api.md)
>
> Create endpoints that operate on one thread

> [card] [Hooks](https://docs.standardagents.ai/core-concepts/hooks.md)
>
> Run code at thread lifecycle points


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.