API keys and identity reference

Updated

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.

  • 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

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. Sign-in for people is in the auth group.

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 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
StatusMeaning
200Successful Response
401No 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.
403The 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.
429rate_limited. Retry-After and the RateLimit-* headers say when to try again.

Response fields

GET /v1/api-keys response fields
FieldTypeRequiredDescription
itemsarray of object ApiKeyyes

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
NameInTypeRequiredNotes
Idempotency-KeyheaderstringnoReplay 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
FieldTypeRequiredDescription
connected_appbooleannoThis 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.
environmentstring (live, test)no
expires_in_daysinteger or nullno
labelstringyes
scopesarray of string or nullnoOmit for the default agent set; '*' for all.

Responses

POST /v1/api-keys responses
StatusMeaning
201Successful Response
401No 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.
403The 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.
409The 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.
422A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule.
429rate_limited. Retry-After and the RateLimit-* headers say when to try again.

Response fields

POST /v1/api-keys response fields
FieldTypeRequiredDescription
agent_idstring or nullyes
connected_appbooleannoThe 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_atstringyes
environmentstring (live, test)yes
expires_atstring or nullyes
idstringyes
labelstringyes
last_used_atstring or nullyes
prefixstringyes
project_idstring or nullyes
revoked_atstring or nullyes
rotated_from_idstring or nullyes
scopesarray of stringyes
secretstringyesShown 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
NameInTypeRequiredNotes
key_idpathstringyes
Idempotency-KeyheaderstringnoReplay 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
StatusMeaning
200Successful Response
401No 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.
403The 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.
404not_found: no such resource for this credential. A resource of another organization answers the same.
409The 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.
422A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule.
429rate_limited. Retry-After and the RateLimit-* headers say when to try again.

Response fields

DELETE /v1/api-keys/{key_id} response fields
FieldTypeRequiredDescription
deletedbooleanno
idstringyes

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
NameInTypeRequiredNotes
key_idpathstringyes
Idempotency-KeyheaderstringnoReplay 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
StatusMeaning
201Successful Response
401No 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.
403The 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.
404not_found: no such resource for this credential. A resource of another organization answers the same.
409The 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.
422A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule.
429rate_limited. Retry-After and the RateLimit-* headers say when to try again.

Response fields

POST /v1/api-keys/{key_id}/rotate response fields
FieldTypeRequiredDescription
agent_idstring or nullyes
connected_appbooleannoThe 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_atstringyes
environmentstring (live, test)yes
expires_atstring or nullyes
idstringyes
labelstringyes
last_used_atstring or nullyes
prefixstringyes
project_idstring or nullyes
revoked_atstring or nullyes
rotated_from_idstring or nullyes
scopesarray of stringyes
secretstringyesShown 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
StatusMeaning
200Successful Response
401No 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.
429rate_limited. Retry-After and the RateLimit-* headers say when to try again.

Response fields

GET /v1/me response fields
FieldTypeRequiredDescription
actorstringyes
environmentstring (live, test)yes
key_idstringyes
key_prefixstringyes
org_idstringyes
project_idstringyes
scopesarray of stringyes

Example

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