# API keys and identity reference

> The API key operations of the Fax for Agents API. Create a key, list keys as metadata, rotate one with an overlap, revoke it, and read who the caller is.

Page: https://faxforagents.com/docs/api/reference/identity/
Last changed: 2026-11-13

Updated Nov 13, 2026

5 operations of the Fax for Agents API, from the OpenAPI document version 1.20.0, snapshot of 2026-11-13. The base URL is `https://api.faxforagents.com/v1`. This is one group of the [full reference](https://faxforagents.com/docs/api/reference/).

- A rotated API key keeps the expires_at of the key it replaces, including no expiry.
- After a rotation the old secret works for up to 24 hours, capped by its own expiry.
- GET /v1/me needs a credential and no scope, so every working API key can call it.

## Operations

- `GET /v1/api-keys` List API keys (metadata only)
- `POST /v1/api-keys` Create an API key (the secret is shown once)
- `DELETE /v1/api-keys/{key_id}` Revoke a key
- `POST /v1/api-keys/{key_id}/rotate` Rotate an API key and preserve its expiry
- `GET /v1/me` Who am I

## What does the identity group cover?

Five operations. Four manage the API keys of an organization, and one tells a caller who it is. Keys for a named agent are minted in the [agents group](https://faxforagents.com/docs/api/reference/agents/). Sign-in for people is in the [auth group](https://faxforagents.com/docs/api/reference/auth/).

## How is an API key created?

`POST /v1/api-keys` takes a label and, when you want them, an environment, a number of days until expiry and a list of scopes. Without scopes the key is given the default set. The schema says `'*'` asks for all of them. The answer is the only one that carries the secret. Store it then. Later reads return the prefix, the scopes, the environment and the dates.

The body also has `connected_app`. It marks a key that an app such as a command line extension uses for a person. The organization's auto-send setting then binds it. The [policies group](https://faxforagents.com/docs/api/reference/policies/) has the setting.

Each API key belongs to one environment. Test keys reach the sandbox and the test wallet. Live keys reach the carrier and real money. The header `X-FFA-Environment` does not change the environment of a key.

## How do I rotate an API key without downtime?

`POST /v1/api-keys/{key_id}/rotate` answers 201 with a new key and its secret. The OpenAPI document states the two rules of a rotation. The replacement keeps the original `expires_at`, including no expiry. The old secret works for up to 24 hours, capped by its existing expiry.

So a rotation is three steps. Rotate, deploy the new secret, and let the old one lapse. The new key names the one it replaced in `rotated_from_id`, which keeps the history readable in the list.

## How do I revoke a key?

`DELETE /v1/api-keys/{key_id}` revokes it at once. There is no overlap. A call with the revoked secret answers 401.

## What does the list of keys return?

`GET /v1/api-keys` returns metadata only. Each row has the id, the label, the prefix, the scopes, the environment and the agent, if there is one. It also says when the key was created, last used, rotated and revoked. The summary of the operation says so in words, "metadata only". No route returns a secret after the call that created it.

## How does a caller find out who it is?

`GET /v1/me` answers with the actor, the organization, the project, the environment, the key id and prefix, and the scopes the credential carries. It needs a valid credential and no scope. Call it first when an API key behaves in a way you did not expect. It shows whether the key is a test or a live one and which scopes it holds.

## Which scope guards API keys?

All four operations on keys need `key:admin`. An agent key is never issued that scope, so an agent cannot create, rotate or revoke a key, its own included. A person signed in to the dashboard, or a key of the organization that holds the scope, runs them.

## Each operation in detail

### GET `/v1/api-keys`

List API keys (metadata only)

Credential: an API key or a signed-in session. Scope: `key:admin`.

#### Responses

GET /v1/api-keys responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/api-keys response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object ApiKey | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/api-keys \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/api-keys`

Create an API key (the secret is shown once)

Credential: an API key or a signed-in session. Scope: `key:admin`.

#### Parameters

POST /v1/api-keys parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

POST /v1/api-keys request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `connected_app` | boolean | no | This key is for a connected app such as the Gemini CLI extension: the auto-send setting binds it. Not available on an agent's key. |
| `environment` | string (live, test) | no |  |
| `expires_in_days` | integer or null | no |  |
| `label` | string | yes |  |
| `scopes` | array of string or null | no | Omit for the default agent set; '*' for all. |

#### Responses

POST /v1/api-keys responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/api-keys response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agent_id` | string or null | yes |  |
| `connected_app` | boolean | no | The key belongs to a connected app used over stdio, such as the Gemini CLI extension. The organization's auto-send setting (Settings, Connected apps) binds it like an OAuth connection. |
| `created_at` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `expires_at` | string or null | yes |  |
| `id` | string | yes |  |
| `label` | string | yes |  |
| `last_used_at` | string or null | yes |  |
| `prefix` | string | yes |  |
| `project_id` | string or null | yes |  |
| `revoked_at` | string or null | yes |  |
| `rotated_from_id` | string or null | yes |  |
| `scopes` | array of string | yes |  |
| `secret` | string | yes | Shown once. Store it now; it cannot be read back. |

#### Example

```
`curl https://api.faxforagents.com/v1/api-keys \ -X POST \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{"label":"example"}'`
```

### DELETE `/v1/api-keys/{key_id}`

Revoke a key

Credential: an API key or a signed-in session. Scope: `key:admin`.

#### Parameters

DELETE /v1/api-keys/{key_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `key_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Responses

DELETE /v1/api-keys/{key_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

DELETE /v1/api-keys/{key_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `deleted` | boolean | no |  |
| `id` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/api-keys/KEY_ID \ -X DELETE \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/api-keys/{key_id}/rotate`

Rotate an API key and preserve its expiry

The replacement keeps the original expires_at, including no expiry. The old secret works for up to 24 hours, capped by its existing expiry.

Credential: an API key or a signed-in session. Scope: `key:admin`.

#### Parameters

POST /v1/api-keys/{key_id}/rotate parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `key_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Responses

POST /v1/api-keys/{key_id}/rotate responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/api-keys/{key_id}/rotate response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agent_id` | string or null | yes |  |
| `connected_app` | boolean | no | The key belongs to a connected app used over stdio, such as the Gemini CLI extension. The organization's auto-send setting (Settings, Connected apps) binds it like an OAuth connection. |
| `created_at` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `expires_at` | string or null | yes |  |
| `id` | string | yes |  |
| `label` | string | yes |  |
| `last_used_at` | string or null | yes |  |
| `prefix` | string | yes |  |
| `project_id` | string or null | yes |  |
| `revoked_at` | string or null | yes |  |
| `rotated_from_id` | string or null | yes |  |
| `scopes` | array of string | yes |  |
| `secret` | string | yes | Shown once. Store it now; it cannot be read back. |

#### Example

```
`curl https://api.faxforagents.com/v1/api-keys/KEY_ID/rotate \ -X POST \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/me`

Who am I

Credential: an API key or a signed-in session.

#### Responses

GET /v1/me responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/me response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `actor` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `key_id` | string | yes |  |
| `key_prefix` | string | yes |  |
| `org_id` | string | yes |  |
| `project_id` | string | yes |  |
| `scopes` | array of string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/me \ -H "Authorization: Bearer ffa_test_..."`
```
