agreements
Agreements are versioned terms of use that a tenant asks its members to accept: an acceptable-use policy, an NDA, data-handling rules.
Agreements are versioned terms of use that a tenant asks its members to accept: an acceptable-use policy, an NDA, data-handling rules. Better IAM records who accepted which version and when, and exposes the result to policies, so you can hold back access until people accept. See the terms of use guide.
Versions, lapses, and enforcement
An agreement starts at version 1. Editing it with newVersion: true publishes the next version, and everyone must
accept again; an edit without it (a typo fix, a new link) keeps existing acceptances valid. An acceptance counts only
while it is for the current version and, when the agreement sets reacceptAfterDays, is younger than that many days
(annual re-acceptance, for example). Each person has one acceptance record per agreement, replaced each time they
accept.
Enforcement is an ordinary policy decision. Every evaluation for a person in their own tenant carries two condition keys:
principal.agreements: the names of the agreements the person has accepted in their current version.principal.pendingAgreements: how manyrequiredagreements they still owe. Service accounts cannot accept anything, so nothing is pending for them.
A deny statement on the count holds back access until every required agreement is accepted:
{
"effect": "deny",
"actions": ["documents:*"],
"resources": ["*"],
"conditions": { "NumericGreaterThan": { "principal.pendingAgreements": 0 } }
}{ "ArrayContains": { "principal.agreements": ["Beta program"] } } grants something only to people who accepted an
optional agreement. Sessions of an assumed role carry neither key, so conditions on them do not match there.
accessPaths.find tells a denied person when accepting their pending
agreements would let them in.
| Method | What it does | Access |
|---|---|---|
accept | Records that you accept the given version of an agreement. | Credential |
create | Publishes a new agreement at version 1, required by default. | Credential |
delete | Deletes an agreement together with every acceptance of it. | Credential |
list | Lists the tenant's agreements by name, with their full text, version, and settings. | Credential |
listMine | Returns every agreement of the tenant with the text and whether you have accepted its current version. | Credential |
status | Reports who accepted an agreement's current version and which active people still owe it. | Credential |
update | Edits an agreement, optionally publishing the change as a new version that everyone must accept again. | Credential |
accept
Records that you accept the given version of an agreement.
Used inTerms of use
- Permission: None beyond an ordinary session of the agreement's tenant.
- Audited as:
agreement:accept, with the agreement's name and version. - Errors:
VERSION_CONFLICT(409) whenversionis not the current version;IMPERSONATION_RESTRICTEDfrom an impersonation session;INVALID_INPUTfor a service account;ACCESS_DENIEDfrom a role session or another tenant's session;NOT_FOUNDwhen the agreement is not in this tenant.
Pass the version you showed the person, from listMine. If the agreement changed in the meantime the call fails,
so nobody accepts text they were not shown. Accepting again restarts the reacceptAfterDays clock. The acceptance
applies from the next authorization check; enforced invariants do not guard it.
const mine = await iam.api.agreements.listMine(credential, { tenantId });
const owed = mine.filter((agreement) => agreement.required && !agreement.accepted);
for (const agreement of owed)
await iam.api.agreements.accept(credential, { tenantId, agreementId: agreement.id, version: agreement.version });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/agreements/accept" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"agreementId": "<agreementId>",
"version": 1
}'iam.api.agreements.accept(
credential: CredentialInput,
input: { tenantId: string; agreementId: string; version: number },
): Promise<{ accepted: boolean; version: number; acceptedAt: number }>create
Publishes a new agreement at version 1, required by default.
Used inTerms of use,Sharing and access questions
- Permission:
iam:agreements:manageon the tenant. - Audited as:
iam:agreements:manage. - Errors:
CONFLICTwhen an agreement with the same name (ignoring case) exists;LIMIT_EXCEEDED(409) when the tenant already has 50;INVALID_INPUTfor an empty name or one over 100 characters, empty content or content over 50 000 characters or with control characters other than tabs and line breaks, aurlthat is not http(s), or areacceptAfterDaysoutside 1 to 3650;INVARIANT_VIOLATIONwhen a new required agreement would make a policy deny someone an enforced invariant says must be allowed.
content is the text people accept (plain text or Markdown); url optionally links to the canonical document.
required: false makes it optional: it never counts toward principal.pendingAgreements, and people who accept it
appear in principal.agreements. Publishing a required agreement raises every person's
principal.pendingAgreements at once: if a policy already denies on that count, people lose the access it covers
until they accept.
await iam.api.agreements.create(credential, {
tenantId,
name: 'Acceptable use',
content: 'Use company systems for work. Report incidents within 24 hours.',
url: 'https://intranet.example.com/policies/acceptable-use',
reacceptAfterDays: 365,
});Prop
Type
A Agreement 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/agreements/create" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"name": "<name>",
"content": "<content>",
"tenantId": "<tenantId>"
}'iam.api.agreements.create(
credential: CredentialInput,
input: AgreementInput & { tenantId: string },
): Promise<Agreement>delete
Deletes an agreement together with every acceptance of it.
- Permission:
iam:agreements:manageon the agreement. - Audited as:
iam:agreements:manage. - Errors:
NOT_FOUNDwhen the agreement is not in this tenant;INVARIANT_VIOLATIONwhen the change would break an enforced invariant.
Its name disappears from principal.agreements and, if it was required, it stops counting toward
principal.pendingAgreements. The acceptance history is gone with it; the audit log keeps the agreement:accept
events.
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/agreements/delete" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"agreementId": "<agreementId>"
}'iam.api.agreements.delete(
credential: CredentialInput,
input: { tenantId: string; agreementId: string },
): Promise<{ deleted: boolean }>list
Lists the tenant's agreements by name, with their full text, version, and settings.
- Permission:
iam:agreements:readon the tenant. - Audited as:
iam:agreements:read.
Prop
Type
An array of Agreement.
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/agreements/list" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.agreements.list(
credential: CredentialInput,
input: { tenantId: string },
): Promise<Agreement[]>listMine
Returns every agreement of the tenant with the text and whether you have accepted its current version.
Used inTerms of use
- Permission: None beyond an ordinary session of the tenant.
- Audited as: Not audited; it only reads.
- Errors:
ACCESS_DENIEDfrom a role session or another tenant's session.
Agreements you still owe come first, required ones before optional ones. Each entry carries accepted plus, when you
accepted some version, acceptedAt and acceptedVersion, so you can tell "never accepted" from "accepted an older
version". Use it to render a banner or an acceptance screen; useAgreements does this in React and Vue apps.
Prop
Type
An array of MyAgreement.
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/agreements/listMine" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.agreements.listMine(
credential: CredentialInput,
input: { tenantId: string },
): Promise<MyAgreement[]>status
Reports who accepted an agreement's current version and which active people still owe it.
- Permission:
iam:agreements:readon the agreement. - Audited as:
iam:agreements:read. - Errors:
NOT_FOUNDwhen the agreement is not in this tenant.
accepted lists people with a current acceptance (version and time). pending lists every other active person,
with acceptedVersion when they accepted an older version or their acceptance lapsed. Only people are reported, not
service accounts. Use it to chase stragglers before you turn on a policy that denies on
principal.pendingAgreements.
Prop
Type
A AgreementStatus 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/agreements/status" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"agreementId": "<agreementId>"
}'iam.api.agreements.status(
credential: CredentialInput,
input: { tenantId: string; agreementId: string },
): Promise<AgreementStatus>update
Edits an agreement, optionally publishing the change as a new version that everyone must accept again.
- Permission:
iam:agreements:manageon the agreement. - Audited as:
iam:agreements:manage. - Errors:
NOT_FOUNDwhen the agreement is not in this tenant;CONFLICTfor a name another agreement uses;INVALID_INPUTfor the same validation ascreate;INVARIANT_VIOLATIONwhen the change would break an enforced invariant.
Fields you omit keep their values. newVersion: true increments the version, so every existing acceptance stops
counting; without it acceptances stay valid even if you change the text. reacceptAfterDays: null removes the lapse,
and an empty url removes the link. A new reacceptAfterDays applies to existing acceptances at once, measured from
when each was given. Policies match principal.agreements by name, so renaming an agreement changes which
statements match it.
// Material change: everyone accepts again.
await iam.api.agreements.update(credential, {
tenantId,
agreementId,
content: 'Use company systems for work. Report incidents within 4 hours.',
newVersion: true,
});Prop
Type
A Agreement 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/agreements/update" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"agreementId": "<agreementId>"
}'iam.api.agreements.update(
credential: CredentialInput,
input: Partial<Omit<AgreementInput, 'reacceptAfterDays'>> & {
tenantId: string;
agreementId: string;
newVersion?: boolean;
reacceptAfterDays?: number | null;
},
): Promise<Agreement>Better IAM is created by Sean Filimon
Last updated