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

# Capability Tokens: Scoped Agent Credentials (afr2_…)

> Learn how Agent Feedback capability tokens work, what claims they carry, how the SDK signs them locally, and what the backend verifies on submission.

A capability token is a short-lived, single-use credential that the SDK generates locally on your server for every eligible product response. It travels inside the `_agentFeedback` envelope as the `submit.authorization` value, giving a customer agent exactly enough authority to submit one outcome review — nothing more. Your product key never leaves your server, and agents never see it.

<Note>
  Your product key (`af_live_...`) is a server-side secret. It never appears in any response body, header, or envelope that reaches an agent. Only the derived, scoped capability token is shared.
</Note>

## Token format

Every capability token uses this three-part dot-separated structure:

```text theme={null}
afr2_<keyId>.<base64url_claims>.<signature>
```

| Segment              | Description                                                                                                                      |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `afr2_<keyId>`       | The `afr2_` prefix identifies the token type; `<keyId>` is the 32-character lowercase hex key ID extracted from your product key |
| `<base64url_claims>` | The base64url-encoded UTF-8 JSON of the claims object (no padding)                                                               |
| `<signature>`        | The base64url-encoded HMAC-SHA256 signature over the first two segments (no padding)                                             |

A real token from the conformance test vector looks like this:

```text theme={null}
afr2_0123456789abcdef0123456789abcdef.eyJ2IjoxLCJpIjoiMDE4ZjFmMmUtN2I0YS03YzEyLTljOGQtMTIzNDU2Nzg5YWJjIiwiaWF0IjoxNzE1MDAwMDAwLCJleHAiOjE3MTUwMDcyMDAsIm4iOiJBUUlEQkFVR0J3Z0pDZ3NNRFE0UEVCRVMifQ.wxJ0YGS21x9eW-Cn33t9V1INhyGNj1_U3qoQns3vdWA
```

## Claims

The claims object is serialized in this exact field order before encoding:

<ParamField body="v" type="number" required>
  Protocol version. Always `1`. The backend uses this to select the correct verification algorithm.
</ParamField>

<ParamField body="i" type="string (UUID)" required>
  The interaction ID. A randomly generated UUID v7 assigned at middleware time. The backend uses this to associate the submitted outcome with the correct interaction record.
</ParamField>

<ParamField body="iat" type="number (Unix timestamp)" required>
  Issued-at time as a Unix timestamp (seconds since epoch). Records when the capability was created.
</ParamField>

<ParamField body="exp" type="number (Unix timestamp)" required>
  Expiry time as a Unix timestamp. Set to no more than two hours after `iat`. The backend rejects any token presented after this time.
</ParamField>

<ParamField body="n" type="string (base64url)" required>
  A cryptographically random 18-byte nonce, base64url-encoded. Included to prevent identical-input collisions even if two capabilities share the same timestamps and interaction ID (which cannot happen in practice, but the nonce provides an additional layer of uniqueness).
</ParamField>

## How the SDK signs a token

The SDK performs all signing locally as part of building the response envelope. No network call is made to Agent Feedback at this stage.

<Accordion title="Signing algorithm (step by step)">
  1. Extract the `keyId` (32 lowercase hex chars) and the full product key from `af_live_<keyId>_<secret>`.
  2. Derive the signing key: `SHA-256(full_product_key)` — the raw digest bytes, not a hex string.
  3. Generate a UUID interaction ID and 18 cryptographically random bytes for the nonce.
  4. Set `iat` to the current Unix timestamp and `exp` to `iat + 7200` (two hours).
  5. Serialize the claims as compact JSON in the field order `v`, `i`, `iat`, `exp`, `n`.
  6. Base64url-encode the UTF-8 bytes of the JSON string (no padding).
  7. Form the signing input: `"afr2_" + keyId + "." + encodedClaims`.
  8. Compute `HMAC-SHA256(signingKey, signingInput)` and base64url-encode the result (no padding).
  9. Return `signingInput + "." + signature`.

  The `conformance.json` file in the protocol repository contains a deterministic test vector you can use to validate your implementation of this algorithm.
</Accordion>

## Product key format

Your product key has this shape:

```text theme={null}
af_live_<32 lowercase hex key ID>_<secret with at least 20 characters>
```

For example:

```text theme={null}
af_live_0123456789abcdef0123456789abcdef_conformance_secret_0123456789abcdef
```

Keep this value in an environment variable and never commit it to source control. The SDK reads it at startup and derives the HMAC signing key from it once.

## Security properties

Because the signing key is derived from your product key on your server, Agent Feedback can verify the capability's authenticity without ever storing your product key in plaintext. The service verifies the HMAC signature on each incoming outcome submission using a one-way digest of your key — your raw product key is never accessible to the verification path.

| Property                   | Mechanism                                                                                           |
| -------------------------- | --------------------------------------------------------------------------------------------------- |
| Authenticity               | HMAC-SHA256 signature; only your server can produce a valid token                                   |
| Expiry                     | The `exp` claim; expired tokens return `401`                                                        |
| Single interaction binding | The `i` claim ties each token to exactly one interaction ID                                         |
| First-write wins           | Duplicate submissions return the original accepted review                                           |
| Key revocation             | A revoked key causes all tokens signed with it to be rejected with `401`                            |
| Cross-workspace isolation  | The backend verifies that the capability belongs to the correct workspace before storing any review |

<Warning>
  A forged, expired, or revoked capability always returns `401 Unauthorized`. The backend never provides a distinguishing error message between these cases, to avoid leaking information about key status.
</Warning>

## What the capability does not contain

The claims carry only the minimum information needed for verification. A capability token never contains:

* Your customer reference or any user identity
* The agent's prompt, instructions, or session context
* Any product response data, query parameters, or content
* Personal data of any kind

This means that even if an agent logs or forwards a capability token, no sensitive product or user data is exposed.
