certifications
Certification campaigns turn periodic access reviews into recorded decisions.
Certification campaigns turn periodic access reviews into recorded decisions. A campaign snapshots the role bindings under review, reviewers keep or revoke each one, and closing the campaign removes the revoked bindings and records what happened to every item, which is the evidence auditors ask for. See the certifications guide for the full workflow.
Campaign permissions are checked on the internal resource iam/certifications/* for tenant-wide calls (create,
list) and on iam/certifications/{campaignId} for one campaign, so you can scope who manages or reviews which
campaign.
Reviewers and who may decide
Every item is one binding as it stood when the campaign opened. Who decides it depends on the campaign's
reviewerMode:
named(the default): the people inreviewerIdsdecide every item withdecide. WhenreviewerIdsis empty, anyone holdingiam:certifications:reviewon the campaign may decide.manager: each person's items are assigned to their manager (Identity.managerId) when that manager is an active, unexpired member of the tenant. Managers decide their items withreview, which needs no certification permission, and find them withlistMine. Items without an active manager, and items held by groups, fall back to the named reviewers (or to anyone holdingiam:certifications:reviewwhen none are named). Throughdecide, a manager-assigned item may be decided only by that manager or by a holder ofiam:certifications:manage.
Nobody decides on their own access, directly or through a group they belong to (SELF_REVIEW). A decision can be
changed until the campaign closes. roleMining.reviewRecommendations
suggests keep or revoke for every item from recorded usage and sign-in activity.
What closing does
Closing applies the campaign in one transaction. Items decided revoke, and undecided items when the campaign's
undecided is revoke, have their binding deleted under the closer's
grant authority; everything else is kept. Each item records an
outcome:
kept: the binding stays.revoked: the binding was removed (with its just-in-time activations), audited asiam:bindings:delete.already-removed: the binding to revoke was already gone, or no longer matched the item's role and subject.revocation-failed: the closer may not remove it. A binding can be removed only by the administrator whose authority issued it or by a root administrator, so a binding someone else granted is left for them, with the reason inoutcomeDetail.
A binding an access package created is removed like any other: a
manual assignment then reports broken, and an automatic assignment gets the binding back at the next reconcile
while the person still matches the rule. To remove birthright access for good, change the rule.
Campaigns created with autoClose: true and a dueAt are closed by the deployment job
iam.closeOverdueCertifications() (CLI close-certifications)
once due, under the creator's authority, audited as certification:auto-close by deployment-operator.
| Method | What it does | Access |
|---|---|---|
close | Closes an open campaign and applies it, removing the bindings reviewers revoked. | Credential |
create | Opens a campaign over the tenant's current role bindings and notifies the reviewers. | Credential |
decide | Records keep or revoke decisions on up to 200 items of an open campaign. | Credential |
delete | Deletes a closed campaign and all its items. | Credential |
get | Returns one campaign with its items and progress. | Credential |
list | Lists the tenant's campaigns, newest first, each with its progress. | Credential |
listMine | Lists the open campaigns that have items assigned to you as a manager, with only those items. | Credential |
remind | Emails every reviewer who still has undecided items a reminder with their pending count. | Credential |
review | Records a manager's keep or revoke decisions on up to 200 items assigned to them, without a certification permission. | Credential |
close
Closes an open campaign and applies it, removing the bindings reviewers revoked.
- Permission:
iam:certifications:manageon the campaign, with recent authentication. - Audited as:
iam:certifications:manage, plus oneiam:bindings:deleteper removed binding. - Errors:
RECENT_AUTH_REQUIREDwhen your sign-in is not recent or the credential is temporary;IMPERSONATION_RESTRICTEDfrom an impersonation session;CONFLICTwhen the campaign is already closed;NOT_FOUNDwhen it is not in this tenant;INVARIANT_VIOLATIONwhen the removals would break an enforced invariant.
The result is the closed campaign with outcomes, the number of items per outcome. Close as the administrator who
granted the bindings, or as root, to avoid revocation-failed items. The closed campaign keeps every decision and
outcome until you delete it.
const closed = await iam.api.certifications.close(credential, { tenantId, campaignId });
// closed.outcomes: { kept: 41, revoked: 6, 'already-removed': 1, 'revocation-failed': 0 }Prop
Type
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/certifications/close" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"campaignId": "<campaignId>"
}'iam.api.certifications.close(
credential: CredentialInput,
input: { tenantId: string; campaignId: string },
): Promise<CertificationCampaign & { outcomes: Record<CertificationOutcome, number> }>create
Opens a campaign over the tenant's current role bindings and notifies the reviewers.
Used inCertifications
- Permission:
iam:certifications:manageoniam/certifications/*. - Audited as:
iam:certifications:manage. - Errors:
LIMIT_EXCEEDED(409) when more than 5000 bindings would be reviewed;INVALID_INPUTfor an empty name, a listed role that is unknown or protected, adueAtthat is not in the future,autoClosewithoutdueAt, or an invalidsubjectType,reviewerMode, orundecided;NOT_FOUNDwhen a reviewer is not in this tenant or is deleted.
The campaign covers the live bindings of every non-protected role, or only of roleIds, optionally only those held
by people or by groups (subjectType). Bindings that have not started yet are left out; eligible bindings are
included and flagged. undecided (default keep) says what closing does with items nobody decided: revoke makes
silence mean removal. When the deployment sends email, each reviewer receives one certification-review message
with their own item count. The result carries the campaign, its progress, and items, the number of bindings it
covers.
const campaign = await iam.api.certifications.create(credential, {
tenantId,
name: 'Q4 admin review',
roleIds: [adminRole.id, billingAdminRole.id],
reviewerMode: 'manager',
reviewerIds: [securityLead.id], // items without an active manager go here
dueAt: Date.parse('2026-12-15T17:00:00Z'),
autoClose: true,
undecided: 'revoke',
});Prop
Type
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/certifications/create" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"name": "<name>"
}'iam.api.certifications.create(
credential: CredentialInput,
input: {
tenantId: string;
name: string;
roleIds?: string[];
subjectType?: 'identity' | 'group';
reviewerIds?: string[];
reviewerMode?: 'named' | 'manager';
dueAt?: number;
autoClose?: boolean;
undecided?: CertificationDecision;
},
): Promise<{
progress: CertificationProgress;
items: number;
name: string;
status: 'open' | 'closed';
createdBy: string;
createdAt: number;
dueAt?: number;
roleIds?: string[];
subjectType?: 'identity' | 'group';
reviewerIds: string[];
reviewerMode?: 'named' | 'manager';
autoClose?: boolean;
undecided: CertificationDecision;
closedAt?: number;
closedBy?: string;
id: string;
tenantId: string;
uniqueKey?: string;
}>decide
Records keep or revoke decisions on up to 200 items of an open campaign.
Used inCertifications
- Permission:
iam:certifications:reviewon the campaign; when the campaign names reviewers, you must be one of them, except for items assigned to you as a manager. - Audited as:
iam:certifications:review. - Errors:
SELF_REVIEWfor an item that certifies your own access;ACCESS_DENIEDwhen you are not a reviewer of the campaign or the item is assigned to someone else's manager and you lackiam:certifications:manage;CONFLICTwhen the campaign is closed;INVALID_INPUTfor an empty batch, more than 200 entries, or a decision other thankeeporrevoke;NOT_FOUNDfor an item that is not in this campaign.
The batch is atomic: one refused entry rejects them all. Deciding an item again replaces the earlier decision and
its note. A note holds at most 500 characters.
await iam.api.certifications.decide(reviewerCredential, {
tenantId,
campaignId,
decisions: [
{ itemId: 'item_1', decision: 'keep' },
{ itemId: 'item_2', decision: 'revoke', note: 'Moved to finance in July' },
],
});Prop
Type
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/certifications/decide" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"campaignId": "<campaignId>",
"decisions": [
{
"itemId": "<itemId>",
"decision": "keep"
}
]
}'iam.api.certifications.decide(
credential: CredentialInput,
input: {
tenantId: string;
campaignId: string;
decisions: CertificationDecisionInput[];
},
): Promise<{ recorded: number }>delete
Deletes a closed campaign and all its items.
- Permission:
iam:certifications:manageon the campaign. - Audited as:
iam:certifications:manage. - Errors:
CONFLICTwhen the campaign is still open (close it first);NOT_FOUNDwhen it is not in this tenant.
Closed campaigns are evidence; delete one only when your retention period for review records has passed.
Prop
Type
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/certifications/delete" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"campaignId": "<campaignId>"
}'iam.api.certifications.delete(
credential: CredentialInput,
input: { tenantId: string; campaignId: string },
): Promise<{ deleted: boolean }>get
Returns one campaign with its items and progress.
- Permission:
iam:certifications:readon the campaign. - Audited as:
iam:certifications:read. - Errors:
NOT_FOUNDwhen the campaign is not in this tenant.
Items are sorted by role and subject, each with its decision, reviewer, and, once closed, its outcome. progress
counts total, decided, keep, and revoke. mine: true leaves out the items that certify your own access
(directly or through a group), which you could not decide anyway.
Prop
Type
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/certifications/get" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"campaignId": "<campaignId>"
}'iam.api.certifications.get(
credential: CredentialInput,
input: { tenantId: string; campaignId: string; mine?: boolean },
): Promise<{
progress: CertificationProgress;
items: CertificationItem[];
name: string;
status: 'open' | 'closed';
createdBy: string;
createdAt: number;
dueAt?: number;
roleIds?: string[];
subjectType?: 'identity' | 'group';
reviewerIds: string[];
reviewerMode?: 'named' | 'manager';
autoClose?: boolean;
undecided: CertificationDecision;
closedAt?: number;
closedBy?: string;
id: string;
tenantId: string;
uniqueKey?: string;
}>list
Lists the tenant's campaigns, newest first, each with its progress.
- Permission:
iam:certifications:readoniam/certifications/*. - Audited as:
iam:certifications:read.
Pass status: 'open' or 'closed' to filter.
Prop
Type
An array of 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/certifications/list" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.certifications.list(
credential: CredentialInput,
input: { tenantId: string; status?: 'open' | 'closed' },
): Promise<{
progress: CertificationProgress;
name: string;
status: 'open' | 'closed';
createdBy: string;
createdAt: number;
dueAt?: number;
roleIds?: string[];
subjectType?: 'identity' | 'group';
reviewerIds: string[];
reviewerMode?: 'named' | 'manager';
autoClose?: boolean;
undecided: CertificationDecision;
closedAt?: number;
closedBy?: string;
id: string;
tenantId: string;
uniqueKey?: string;
}[]>listMine
Lists the open campaigns that have items assigned to you as a manager, with only those items.
- Permission: None beyond an ordinary session of the tenant (not an assumed role or another tenant's session).
- Audited as: Not audited; it only reads.
- Errors:
ACCESS_DENIEDfrom a role session or a session of another tenant.
This is the data a manager's review screen needs. It returns only manager-mode assignments; named reviewers use
get with mine: true instead.
Prop
Type
An array of 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/certifications/listMine" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.certifications.listMine(
credential: CredentialInput,
input: { tenantId: string },
): Promise<{
id: string;
name: string;
dueAt?: number;
reviewerMode: 'named' | 'manager';
items: CertificationItem[];
progress: CertificationProgress;
}[]>remind
Emails every reviewer who still has undecided items a reminder with their pending count.
- Permission:
iam:certifications:manageon the campaign. - Audited as:
iam:certifications:manage, pluscertification:remindwith the counts. - Errors:
DELIVERY_REQUIREDwhen the deployment has no email delivery callback;CONFLICTwhen the campaign is closed;NOT_FOUNDwhen it is not in this tenant.
Managers are reminded of their assigned items and named reviewers of the undecided items that fall back to them.
Only active reviewers with an email address are counted. When the campaign names no reviewers, nobody is reminded of
unassigned items. Returns reminded (people emailed) and pending (undecided items). Send one a few days before
dueAt.
Prop
Type
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/certifications/remind" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"campaignId": "<campaignId>"
}'iam.api.certifications.remind(
credential: CredentialInput,
input: { tenantId: string; campaignId: string },
): Promise<{ reminded: number; pending: number }>review
Records a manager's keep or revoke decisions on up to 200 items assigned to them, without a certification permission.
- Permission: None beyond an ordinary, non-impersonated session of the tenant; every item must be assigned to you.
- Audited as:
certification:review, with the number of keep and revoke decisions. - Errors:
ACCESS_DENIEDfor an item that is not assigned to you, or from a role session or another tenant's session;IMPERSONATION_RESTRICTEDfrom an impersonation session;SELF_REVIEWfor your own access;CONFLICTwhen the campaign is closed;INVALID_INPUTfor an empty batch, more than 200 entries, or an invalid decision.
This lets line managers take part in reviews without holding an administrator role: being assigned the item is the
authorization. Like decide, the batch is atomic and a later decision replaces an earlier one.
await iam.api.certifications.review(managerCredential, {
tenantId,
campaignId,
decisions: [{ itemId, decision: 'revoke', note: 'No longer on the payments team' }],
});Prop
Type
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/certifications/review" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"campaignId": "<campaignId>",
"decisions": [
{
"itemId": "<itemId>",
"decision": "keep"
}
]
}'iam.api.certifications.review(
credential: CredentialInput,
input: {
tenantId: string;
campaignId: string;
decisions: CertificationDecisionInput[];
},
): Promise<{ recorded: number }>Better IAM is created by Sean Filimon
Last updated