# Accept agents at your own API

Verify an AI agent's access token in your service, check that it was issued for your API, and confirm the one action it is bound to.

When an AI agent calls your API, it presents an access token. This guide shows you how to verify that token in your service, read who is calling, and check the action the agent is bound to. Your service does all of this without calling AuthFI.

## Before you begin

You need:

- A registered agent. See [Give an AI agent an identity](/quickstart/agent).
- Administrator access to the AuthFI console.
- A JWT library that supports `EdDSA`. The examples use `jose` for Node.js and `PyJWT` for Python.

<Steps>

### Give the agent a token for your API

In the console, open **Integrations > Applications** and register your API as an application. Then grant the agent access to the application registered for your API.

The application's client ID is how a token names your API. It is in the header of the application's page, and under "Client ID" in the **Overview** section.

The agent then asks for an access token for your API, with the client ID as `resource`:

```bash
curl https://acme.authfi.io/apis/regional/v1/ai/auth/token \
  -d grant_type=client_credentials \
  -d client_id=agt_7f3a9c21 \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  -d client_assertion="$ASSERTION" \
  -d resource="$API_CLIENT_ID"
```

`$ASSERTION` is a fresh client assertion, the short statement the agent signs with its own key. Build it as in [Give an AI agent an identity](/quickstart/agent). `$API_CLIENT_ID` is the client ID of your API's application. `agt_7f3a9c21` is the agent ID, shown as "Client id" on the agent's **Connect** tab under **Identity > AI Agents**. The same tab shows the token endpoint and a worked example of this request.

```json
{
  "access_token": "eyJhbGciOiJFZERTQSIs…",
  "token_type": "Bearer",
  "expires_in": 900,
  "agent_id": "agt_7f3a9c21",
  "type": "autonomous",
  "capabilities": ["transfer:accounts"]
}
```

An agent with no grant on your API gets `invalid_target`. It cannot obtain a token that your API would accept.

### Verify the access token

In your service, check every access token before you act on it.

| Check     | Expect                                                                                    |
| --------- | ----------------------------------------------------------------------------------------- |
| Signature | Verifies with the key named by `kid`, from `https://acme.authfi.io/.well-known/jwks.json` |
| `alg`     | `EdDSA`                                                                                   |
| `typ`     | `at+jwt`                                                                                  |
| `iss`     | `https://acme.authfi.io`                                                                  |
| `aud`     | Your API's client ID                                                                      |
| `exp`     | In the future                                                                             |

<Tabs labels={["Node.js", "Python"]}>

```js
import { createRemoteJWKSet, jwtVerify } from "jose";

const keys = createRemoteJWKSet(new URL("https://acme.authfi.io/.well-known/jwks.json"));

export async function verifyAgent(token) {
  const { payload } = await jwtVerify(token, keys, {
    issuer: "https://acme.authfi.io",
    audience: process.env.API_CLIENT_ID,
    algorithms: ["EdDSA"],
    typ: "at+jwt",
  });
  return payload;
}
```

```python
import os, jwt
from jwt import PyJWKClient

keys = PyJWKClient("https://acme.authfi.io/.well-known/jwks.json")

def verify_agent(token: str) -> dict:
    key = keys.get_signing_key_from_jwt(token)
    claims = jwt.decode(
        token,
        key.key,
        algorithms=["EdDSA"],
        issuer="https://acme.authfi.io",
        audience=os.environ["API_CLIENT_ID"],
    )
    if jwt.get_unverified_header(token).get("typ") != "at+jwt":
        raise jwt.InvalidTokenError("not an access token")
    return claims
```

</Tabs>

Both libraries cache the key set and fetch it again when they meet a `kid` they do not know. That is what [key rotation](/security/keys) needs.

### Read who is calling

The verified claims tell you which agent is calling and who is accountable for it.

| Claim               | Tells you                                                             |
| ------------------- | --------------------------------------------------------------------- |
| `client_id`         | The agent, always. `agt_…`                                            |
| `sub`               | The agent, or the person it is acting for                             |
| `act.sub`           | Present when the agent acts for a person: the agent, as `agent:agt_…` |
| `agent_type`        | `delegated` or `autonomous`                                           |
| `owner_user_id`     | The owner of a delegated agent                                        |
| `approval_group_id` | The approval group of an autonomous agent                             |
| `capabilities`      | The names of what the agent was granted                               |
| `roles`, `groups`   | The agent's own roles and groups                                      |

An agent calls either as itself or for a person. To tell the two cases apart, look for `act`.

| Token              | `sub`      | `act.sub`     | Means                               |
| ------------------ | ---------- | ------------- | ----------------------------------- |
| Agent on its own   | `agt_…`    | absent        | The agent is acting as itself       |
| Agent for a person | the person | `agent:agt_…` | The agent is acting for that person |

A token with `act` is issued after the person [approves the agent's request](/agents/ciba).

Decide what the caller may do from `capabilities`, and record `client_id` with everything it does.

### Check the action the agent is bound to

For a sensitive action, require a [transaction token](/agents/approvals#bind-a-token-to-the-action) as well as the access token. A transaction token names one action and its details, and lasts 60 seconds.

| Check                          | Expect                                          |
| ------------------------------ | ----------------------------------------------- |
| Signature, `iss`, `aud`, `exp` | As for the access token                         |
| `typ`                          | `txn_token+jwt`                                 |
| `sub`                          | The same agent as the access token              |
| `azd.action`                   | The action being asked for                      |
| `azd.resource`                 | The thing it is being done to                   |
| `azd.context`                  | The same details as the request in front of you |

```json
{
  "typ": "txn_token+jwt",
  "sub": "agt_7f3a9c21",
  "txn": "0b7d6a52-…",
  "azd": {
    "action": "transfer:accounts",
    "resource": "account:4821",
    "context": { "amount": 1200, "currency": "USD", "account_type": "business" }
  }
}
```

Compare `azd` with what the request asks for. A token issued for 1,200 must not carry a request for 12,000. Reject the request if anything differs.

</Steps>

## Check that it works

Pass the access token from the first step to the function from the second step, and print the result.

<Tabs labels={["Node.js", "Python"]}>

```js
console.log(await verifyAgent(process.env.ACCESS_TOKEN));
```

```python
print(verify_agent(os.environ["ACCESS_TOKEN"]))
```

</Tabs>

The claims are printed. `client_id` is the agent ID, `iss` is `https://acme.authfi.io`, and `aud` names your API's client ID.

Then pass a token that was issued for another service. Verification fails, because `aud` does not name your API.

## When an agent is revoked

An agent's access token lasts 15 minutes. A revoked agent loses access to your API within that time, with nothing for you to do. To reject it sooner, receive [revocation events](/agents/revocation#receive-revocation-events).

## Troubleshooting

| What you see                                  | Cause                                                            | Fix                                                                      |
| --------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `invalid_target` from the token endpoint      | The agent has no grant on your API                               | Grant the agent access to the application registered for your API        |
| `invalid_client` from the token endpoint      | The agent is unknown or revoked, or its assertion did not verify | Check the key ID, the audience and the clock. Use a new `jti` each time. |
| Verification fails on the audience            | The token was issued for another service                         | Have the agent send your API's client ID as `resource`                   |
| Verification fails on a `kid` you do not know | The project's signing key was rotated                            | Fetch the key set again, then cache it                                   |

## Next steps

<Cards>
  <Card href="/agents/revocation" title="Revoke an agent">
    Receive an event the moment an agent is revoked.
  </Card>
  <Card href="/agents/approvals" title="Ask before a sensitive action">
    How an agent gets the transaction token your API checks.
  </Card>
  <Card href="/reference/tokens" title="Tokens and claims">
    Every claim in an agent access token.
  </Card>
  <Card href="/security/keys" title="Keys and secrets">
    What your service must do when a signing key rotates.
  </Card>
</Cards>
