BetterIAM
Server API

credentials

Credentials manages API keys: opaque bearer tokens that let a service account call Better IAM and your product without a person signing in.

Credentials manages API keys: opaque bearer tokens that let a service account call Better IAM and your product without a person signing in. Keys carry a label, record when they were last used, can be limited to a list of actions, and always expire, so you can find and remove the ones nobody needs. People never get API keys; they use sessions. The API key hygiene guide covers the routine.

What a key can do

A key acts as its service account: it carries the account's roles, group memberships, and relations, and policies see principal.kind as service and principal.sessionKind as api-key. Present it as Authorization: Bearer <key> over HTTP, or as { token } in server calls. Two limits apply on top of the account's grants, and both only ever narrow access:

  • Scopes or a session policy. scopes: ['documents:read'] compiles into a policy that allows exactly those actions on every resource; policy accepts a full policy document instead. Either one acts as a boundary: the key can do only what both the account's roles and the policy allow.
  • The issuer's grant authority. A key is tied to the grant authority of the administrator who issued it and stays within that authority's ceiling. If the authority is revoked (for example when that administrator is offboarded), the key stops working.

A session token the key mints with sts.getSessionToken keeps both limits. A role session it assumes with roles.assume does not: the key's scopes only decide whether it may assume the role (iam:roles:assume), and the role session then acts with the role's permissions, bounded by the trust's ceiling and its own session policy. Scope the role or the trust, not only the key.

A key is refused as soon as its account is disabled, expires, or is deleted. Operations that require recent authentication, such as creating webhooks or keys, accept a key only during the first minutes after it was issued or rotated (five minutes by default), because a key's authentication time is its creation time.

Keeping keys clean

Every key has a name (at most 128 characters) and description for reviews, an expiresAt (90 days by default, at most a year), and a lastUsedAt that is recorded at most once a minute and absent until first use. list with unusedForMs finds keys nobody uses, analysis.findings reports them as stale-api-key, and reports.access lists keys unused or ending soon. Expired keys stay listed with expired: true until you renew or revoke them. Token material is never returned after create and rotate; only a hash is stored.

Methods6
Serveriam.api.credentials
Clientclient.credentials
HTTPPOST /api/iam/credentials/*
MethodWhat it doesAccess
createIssues an API key for an active service account and returns its token, which is shown only once.Credential
getReturns one API key's label, lifetime, scopes, and last use, without token material.Credential
listLists the API keys of the tenant or of one service account, newest first, optionally only the unused ones.Credential
revokeDeletes an API key so it stops working immediately.Credential
rotateReplaces an API key with a new token in one transaction, so the old token stops working the moment the new one exists.Credential
updateRelabels an API key or moves its expiry; the token itself does not change.Credential

create

Issues an API key for an active service account and returns its token, which is shown only once.

POST/api/iam/credentials/create
client.credentials.create()Credential

Used inAI agents,Advanced,Sign-in methods,Temporary access,Access lifecycle,Access lifecycle

  • Permission: iam:credentials:create on the service account (iam/{identityId}), with recent authentication and an active grant authority.
  • Audited as: iam:credentials:create, on the service account.
  • Errors: INVALID_IDENTITY when the identity is not an active, unexpired service account; NOT_FOUND when it is not in this tenant; INVALID_INPUT for both scopes and policy, an empty scopes list, or expiresInSeconds outside 60 seconds to 365 days; INVALID_ACTION or INVALID_POLICY when the scopes or policy do not validate against the catalog; GRANT_AUTHORITY_REQUIRED when the caller holds no grant authority; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

Because the permission is checked on the service account, you can let a team issue keys for its own integration accounts only. Give every key the narrowest scopes that work and a name that says where it is deployed.

New keys are 58 characters that start with biam_key_ and end in a checksum, so secret scanners (and the credentialTokenScanPattern export of @better-iam/auth) can recognize a leaked one; keys issued before the format existed keep working until they expire. For short-lived, narrower credentials derived from a key, such as one per CI job, use sts.getSessionToken with the key as the caller.

const { token, credentialId, expiresAt } = await iam.api.credentials.create(credential, {
  tenantId,
  identityId: deployBotId,
  name: 'github-actions',
  description: 'Release workflow in acme/api',
  scopes: ['deployments:create', 'deployments:read'],
  expiresInSeconds: 30 * 86400,
});
// Store `token` in the CI secret store now; it cannot be read again.
Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/credentials/create" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "identityId": "<identityId>"
}'
Signature
iam.api.credentials.create(
  credential: CredentialInput,
  input: {
    tenantId: string;
    identityId: string;
    expiresInSeconds?: number;
    policy?: PolicyDocument;
    scopes?: string[];
    name?: string;
    description?: string;
  },
): Promise<{
  token: string;
  credentialId: string;
  expiresAt: number;
  name: string | undefined;
}>

get

Returns one API key's label, lifetime, scopes, and last use, without token material.

POST/api/iam/credentials/get
client.credentials.get()Credential
  • Permission: iam:credentials:read on the key.
  • Audited as: iam:credentials:read.
  • Errors: NOT_FOUND when the key is not in this tenant; INVALID_CREDENTIAL when the id belongs to a session that is not an API key.

scopes is present when the key was issued with scopes (its policy is exactly that list); otherwise policy shows the session policy, if any. credentialAuthorityId names the grant authority the key was issued under.

Input

Prop

Type

Returns

A CredentialSummary object:

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/credentials/get" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "credentialId": "<credentialId>"
}'
Signature
iam.api.credentials.get(
  credential: CredentialInput,
  input: { tenantId: string; credentialId: string },
): Promise<CredentialSummary>

list

Lists the API keys of the tenant or of one service account, newest first, optionally only the unused ones.

POST/api/iam/credentials/list
client.credentials.list()Credential

Used inAccess lifecycle,Access lifecycle

  • Permission: iam:credentials:read on the service account when identityId is given, otherwise on the tenant.
  • Audited as: iam:credentials:read.
  • Errors: INVALID_INPUT when unusedForMs is negative or more than ten years.

unusedForMs keeps only keys that have not authenticated a request in that long, counting keys never used since they were issued. Expired keys are included, marked expired: true.

const stale = await iam.api.credentials.list(credential, { tenantId, unusedForMs: 60 * 86_400_000 });
for (const key of stale)
  await iam.api.credentials.revoke(credential, { tenantId, credentialId: key.id });
Input

Prop

Type

Returns

An array of CredentialSummary.

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/credentials/list" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>"
}'
Signature
iam.api.credentials.list(
  credential: CredentialInput,
  input: { tenantId: string; identityId?: string; unusedForMs?: number },
): Promise<CredentialSummary[]>

revoke

Deletes an API key so it stops working immediately.

POST/api/iam/credentials/revoke
client.credentials.revoke()Credential

Used inAccess lifecycle,Access lifecycle

  • Permission: iam:credentials:revoke on the key, with recent authentication.
  • Audited as: iam:credentials:revoke.
  • Errors: NOT_FOUND when the key is not in this tenant; INVALID_CREDENTIAL for any other kind of session (user sessions and role sessions end through their own calls); RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

Revocation cannot be undone; issue a new key if the integration still needs access. To stop every key of an account at once, disable the account with serviceAccounts.setStatus.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/credentials/revoke" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "credentialId": "<credentialId>"
}'
Signature
iam.api.credentials.revoke(
  credential: CredentialInput,
  input: { tenantId: string; credentialId: string },
): Promise<{ revoked: boolean }>

rotate

Replaces an API key with a new token in one transaction, so the old token stops working the moment the new one exists.

POST/api/iam/credentials/rotate
client.credentials.rotate()Credential
  • Permission: iam:credentials:create on the key, with recent authentication.
  • Audited as: iam:credentials:create.
  • Errors: NOT_FOUND when the key is not in this tenant; INVALID_CREDENTIAL when the id is not an API key; ACCESS_DENIED unless the caller issued the key (holds its grant authority) or is the platform root, or when that authority has been revoked; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

The replacement gets a new credentialId and keeps the name, description, scopes or policy, and expiry of the old key. Its usage history starts over, so it shows as unused until the integration uses it. Rotate on a schedule, or at once when a token may have leaked; deploy the returned token before anything else, because the old one is already dead.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/credentials/rotate" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "credentialId": "<credentialId>"
}'
Signature
iam.api.credentials.rotate(
  credential: CredentialInput,
  input: { tenantId: string; credentialId: string },
): Promise<{
  token: string;
  credentialId: string;
  expiresAt: number;
  name: string | undefined;
}>

update

Relabels an API key or moves its expiry; the token itself does not change.

POST/api/iam/credentials/update
client.credentials.update()Credential
  • Permission: iam:credentials:create on the key; changing expiresAt also requires recent authentication and the key's grant authority (the issuer, or the platform root).
  • Audited as: iam:credentials:create.
  • Errors: INVALID_INPUT when nothing is given to change, or expiresAt is not in the future or is more than a year away; NOT_FOUND; INVALID_CREDENTIAL; ACCESS_DENIED when changing the expiry of a key issued under another administrator's authority; RECENT_AUTH_REQUIRED when changing the expiry without recent authentication.

Pass null for name or description to clear it. expiresAt can shorten a key's life or extend it, including renewing a key that has already expired, which then works again without a new token.

Input

Prop

Type

Returns

A CredentialSummary object:

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/credentials/update" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "credentialId": "<credentialId>"
}'
Signature
iam.api.credentials.update(
  credential: CredentialInput,
  input: {
    tenantId: string;
    credentialId: string;
    name?: string | null;
    description?: string | null;
    expiresAt?: number;
  },
): Promise<CredentialSummary>

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page