# guests (/docs/reference/api/guests)

> Guests are people from outside an organization, or from another tenant, who work inside it for a limited time.



Guests are people from outside an organization, or from another tenant, who work inside it for a limited time. An
organization invites them by email, a sponsor vouches for each one and renews their access, and access ends unless it
is renewed. A guest is an ordinary person (`kind: 'user'`) of the host tenant with a server-owned `guest` marker, so
roles, groups, access packages, separation of duties, invariants, certifications, expiry, and offboarding apply to
guests unchanged, and policies can single them out with `principal.guest`, `principal.guestSponsorId`, and
`principal.homeTenantId`. The repository guide is
[Guests](https://github.com/Better-IAM/better-iam/blob/main/docs/guests.md).

## Guests and sponsors [#guests-and-sponsors]

Every guest has a sponsor: an active, unexpired person of the host tenant who is not a guest. `invite` makes the caller
the sponsor unless `sponsorId` names someone else. The sponsor renews the guest's access with [`attest`](#attest)
without any permission and sees their guests with [`mine`](#mine). A guest's access ends at the identity's
`expiresAt` (90 days after redemption by default); from then on every credential of the guest is refused, and
everything the invitation granted ends with it. When a sponsor leaves, offboarding with a successor hands their guests
over, and otherwise the guests are flagged `sponsorMissing` until an administrator calls [`setSponsor`](#setsponsor).

A guest account's `accountStatus` is `active`, `expired` (the access ended), `removed` (by [`remove`](#remove) or
offboarding), or `converted` (by [`convertToMember`](#converttomember)).

## Cross-tenant access settings [#cross-tenant-access-settings]

Each tenant has inbound settings (whether it accepts guests, allowed and blocked email domains, and partner
organizations admitted or refused over those defaults) and outbound settings (whether people at its verified domains
may become guests elsewhere, with partner exceptions). An address whose domain another tenant verified belongs to that
tenant: the guest is tenant-sourced (`homeTenantId`), and both the host's inbound and the home tenant's outbound
settings must admit them. Admission is checked when an invitation is sent, sent again, and redeemed, and a refusal is
`GUEST_NOT_ALLOWED`. The settings also hold the default access and review periods and an optional `guestBoundary`, a
policy that caps every guest's decisions in the tenant. See [`configure`](#configure).

## Who may call what [#who-may-call-what]

Administrators hold `iam:guests:read`, `iam:guests:invite`, `iam:guests:manage`, or `iam:guests:settings` on
`iam/guests`, `iam/guests/{identityId}`, `iam/guests/invitations`, `iam/guests/invitations/{id}`, and
`iam/guests/settings`. `setSponsor`, `remove`, `convertToMember`, and `configure` also need a recent sign-in. `redeem`
is public, `mine` needs only a person's own session or API key in the tenant, and a guest's sponsor calls `attest`
without any permission. Guests hold none of these rights over other guests, renew nobody's access, choose no
sponsor, and make nobody a member, themselves included.

| Method                                  | What it does                                                                                                                                             | Access     |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| [`attest`](#attest)                     | Renews a guest's access, together with everything the invitation granted.                                                                                | Credential |
| [`configure`](#configure)               | Changes the tenant's cross-tenant access settings: who may join as a guest, where its people may be guests, the default periods, and the guest boundary. | Credential |
| [`convertToMember`](#converttomember)   | Makes an active guest an ordinary member of the organization.                                                                                            | Credential |
| [`get`](#get)                           | Returns one guest with their sponsor, invitation, renewal, access end, and last sign-in.                                                                 | Credential |
| [`getSettings`](#getsettings)           | Returns the tenant's cross-tenant access settings, or the defaults until they are configured.                                                            | Credential |
| [`invite`](#invite)                     | Invites a person from outside the organization as a guest and emails them a personal invitation.                                                         | Credential |
| [`list`](#list)                         | Lists the tenant's guests by name, with their sponsor, access end, next review, and last sign-in.                                                        | Credential |
| [`listInvitations`](#listinvitations)   | Lists the tenant's guest invitations, newest first, without their tokens.                                                                                | Credential |
| [`mine`](#mine)                         | Lists the active guests the caller sponsors, the soonest review first.                                                                                   | Credential |
| [`redeem`](#redeem)                     | Redeems a guest invitation: creates the guest with the invited address and a password, grants what the invitation grants, and signs them in.             | Public     |
| [`remove`](#remove)                     | Removes a guest: disables them and takes away everything they hold, in one transaction.                                                                  | Credential |
| [`resendInvitation`](#resendinvitation) | Sends a pending or lapsed guest invitation again with a new token and a new lifetime.                                                                    | Credential |
| [`revokeInvitation`](#revokeinvitation) | Withdraws a pending guest invitation, so its token stops working.                                                                                        | Credential |
| [`setSponsor`](#setsponsor)             | Gives an active guest a new sponsor and clears a missing-sponsor flag.                                                                                   | Credential |

## attest [#attest]

Renews a guest's access, together with everything the invitation granted.

**HTTP:** `POST /api/iam/guests/attest` (requires a credential) · **Browser client:** `client.guests.attest()`

* **Permission:** None for the guest's sponsor in their own session or API key (never while impersonating); anyone
  else needs `iam:guests:manage` on `iam/guests/{identityId}`.
* **Audited as:** `guest:attest` with the new end, the next review, and `asSponsor`; an administrator's call also as
  `iam:guests:manage`.
* **Errors:** `INVALID_TRANSITION` (409) when the guest is disabled or their access ended; `ACCESS_DENIED` for a guest
  (their own access included), for a sponsor calling from anything but their own session or API key in the tenant,
  or for anyone else without the permission; `TENANT_UNAVAILABLE` for a sponsor's renewal while the tenant or an ancestor is not active; `IMPERSONATION_RESTRICTED` for a sponsor who is
  being impersonated; `INVALID_INPUT` for `days` outside 1 to 365; `NOT_FOUND`.

The access then ends `days` from now (the tenant's `accessDays` by default), and the next review is due after the
tenant's `reviewEveryDays`, or at the new end if that is sooner. The invitation's bindings, memberships, and package
assignments move to the new end, even when an administrator changed the guest's `expiresAt` in between; grants whose
end was changed by hand keep theirs, and a package assignment keeps its end when the package's maximum duration does
not allow the new one. A package assignment lengthens only the memberships it created at redemption, so a group the
package gained since, or a membership the guest holds another way, keeps its end. Because a renewal moves only what
the invitation granted and ends again within a year, it needs no `iam:licenses:assign` for licensed groups. To bring
back a guest whose access ended, extend `expiresAt` and re-enable them with `identities.update` and
`identities.setStatus`.

```ts
await iam.api.guests.attest(sponsorSession, { tenantId, identityId: ginaId, days: 90 });
```

```ts title="Signature"
iam.api.guests.attest(
  credential: CredentialInput,
  input: { tenantId: string; identityId: string; days?: number },
): Promise<GuestView>
```

## configure [#configure]

Changes the tenant's cross-tenant access settings: who may join as a guest, where its people may be guests, the default periods, and the guest boundary.

**HTTP:** `POST /api/iam/guests/configure` (requires a credential) · **Browser client:** `client.guests.configure()`

* **Permission:** `iam:guests:settings` on `iam/guests/settings`, and a recent sign-in.
* **Audited as:** `iam:guests:settings`, plus `guest:settings` with the changed setting names and the settings after
  the change (whether a boundary is set, not the document).
* **Errors:** `INVALID_INPUT` for an unknown setting (at the top level or inside `inbound` or `outbound`), nothing to
  change, a flag that is not a boolean, a malformed domain, a domain or partner list of more than 100 entries, a
  partner entry naming this tenant, an unknown tenant, or a tenant already listed, an `accessDays` outside 1 to 365, or
  a `reviewEveryDays` outside 7 to 365;
  `INVALID_POLICY` or `INVALID_ACTION` for a malformed `guestBoundary`; `RECENT_AUTH_REQUIRED`.

Only the fields given change, inside `inbound` and `outbound` too, and `guestBoundary: null` removes the boundary.
Domains match themselves and their subdomains and are stored lowercase. On the inbound side a refusing partner entry
wins, then a blocked domain, then an admitting partner entry, then `allowGuests` and `allowedDomains`. On the outbound
side a partner entry for the host decides, else `allowGuestInvitations`. The other organization's refusal is reported
without revealing its settings. The boundary is a ceiling like a tenant boundary: it grants nothing and applies to
guests only.

```ts
await iam.api.guests.configure(credential, {
  tenantId,
  inbound: { allowedDomains: ['partner.example'], partners: [{ tenantId: globexId, allow: true }] },
  reviewEveryDays: 30,
  guestBoundary: {
    version: 1,
    statements: [{ effect: 'allow', actions: ['documents:read'], resources: ['document/shared-*'] }],
  },
});
```

```ts title="Signature"
iam.api.guests.configure(
  credential: CredentialInput,
  input: GuestSettingsInput,
): Promise<GuestSettingsView>
```

## convertToMember [#converttomember]

Makes an active guest an ordinary member of the organization.

**HTTP:** `POST /api/iam/guests/convertToMember` (requires a credential) · **Browser client:** `client.guests.convertToMember()`

* **Permission:** `iam:guests:manage` on `iam/guests/{identityId}`, and a recent sign-in. With `clearExpiry`, also
  `iam:licenses:assign` when a license product is assigned to a group whose membership would lose its end.
* **Audited as:** `iam:guests:manage`, plus `guest:convert` with `clearExpiry` and the previous access end.
* **Errors:** `INVALID_TRANSITION` (409) unless the guest is active, enabled, and within their access;
  `INVALID_INPUT` for a `clearExpiry` that is not a boolean; `ACCESS_DENIED` for `clearExpiry` on a licensed group
  without `iam:licenses:assign` (audited as a denial); `RECENT_AUTH_REQUIRED`; `NOT_FOUND`.

The `guest` marker goes, so policies see `principal.guest: false` from the next request (the same session included)
and the guest boundary no longer applies. The guest account closes as `converted`, and birthright rules run again for
the new member. The access end stays unless `clearExpiry: true` removes it together with the end of what the
invitation granted. The result is the public identity.

```ts title="Signature"
iam.api.guests.convertToMember(
  credential: CredentialInput,
  input: { tenantId: string; identityId: string; clearExpiry?: boolean },
): Promise<PublicIdentity>
```

## get [#get]

Returns one guest with their sponsor, invitation, renewal, access end, and last sign-in.

**HTTP:** `POST /api/iam/guests/get` (requires a credential) · **Browser client:** `client.guests.get()`

* **Permission:** `iam:guests:read` on `iam/guests/{identityId}`.
* **Audited as:** `iam:guests:read`.
* **Errors:** `NOT_FOUND` when the identity is not a guest of this tenant (members and deleted identities included).

A converted guest is still returned, with `accountStatus: 'converted'`.

```ts title="Signature"
iam.api.guests.get(
  credential: CredentialInput,
  input: { tenantId: string; identityId: string },
): Promise<GuestView>
```

## getSettings [#getsettings]

Returns the tenant's cross-tenant access settings, or the defaults until they are configured.

**HTTP:** `POST /api/iam/guests/getSettings` (requires a credential) · **Browser client:** `client.guests.getSettings()`

* **Permission:** `iam:guests:read` on `iam/guests/settings`.
* **Audited as:** `iam:guests:read`.

`configured` is false while the tenant uses the defaults: guests accepted from any domain, the tenant's people free to
become guests elsewhere, 90 days of access, and a review every 90 days.

```ts title="Signature"
iam.api.guests.getSettings(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<GuestSettingsView>
```

## invite [#invite]

Invites a person from outside the organization as a guest and emails them a personal invitation.

**HTTP:** `POST /api/iam/guests/invite` (requires a credential) · **Browser client:** `client.guests.invite()`

* **Permission:** `iam:guests:invite` on `iam/guests/invitations`, plus, for what the invitation grants,
  `iam:bindings:create` on each role, `iam:groups:update` on each group with the authorities behind the group's
  bindings, `iam:packages:assign` on each package with the rights to assign it by hand, and `iam:licenses:assign`
  when a license product is assigned to one of the groups or of the packages' groups (checked again at redemption).
  Naming someone other than yourself as `sponsorId` also needs `iam:guests:manage` on `iam/guests/invitations`.
* **Audited as:** `iam:guests:invite`, plus `guest:invite` with the address, sponsor, grants, access days, and lapse.
* **Errors:** `DELIVERY_REQUIRED` without an email delivery callback; `TENANT_INACTIVE` when the tenant is not
  active; `CONFLICT` (409) when someone in the tenant has the address or it has a pending invitation;
  `GUEST_NOT_ALLOWED` (403) for an address at one of the tenant's own verified domains, or when the inbound or the home organization's outbound settings refuse it; `INVALID_SPONSOR`
  when `sponsorId` is not an active member who is not a guest, or is left out by a caller who cannot sponsor (a root
  administrator, a service account's key, a delegated session); `IMPERSONATION_RESTRICTED`; `GRANT_AUTHORITY_REQUIRED`
  for roles or groups without a grant authority; `PROTECTED_RESOURCE` for a protected role; `TEAM_MANAGED` for a
  team's backing group; `NOT_FOUND` for a role, group, or package outside the tenant; `ACCESS_DENIED` without the
  rights to grant them, without `iam:guests:manage` when naming someone else as the sponsor, or from an assumed role
  (redemption re-checks the inviter's own rights); `INVALID_INPUT` for a malformed email or one whose domain is not a
  DNS name (the domain is compared and stored lowercase, in punycode, without a trailing dot), an `accessDays` outside 1 to 365, an `expiresInDays`
  outside 1 to 30, a `message` over 1000 characters or with line breaks, more than 100 roles, groups, or packages, or
  the platform tenant.

The email (template `guest-invitation`) carries a `biam_gst_…` token that is stored only as a hash and returned
nowhere else; point `links.guestInvitation` in the email templates at the page that calls [`redeem`](#redeem). The
invitation lapses after `expiresInDays` (14 by default), and the guest's access lasts `accessDays` from redemption
(the tenant's default, 90 unless configured). Roles and groups are granted at redemption under the inviter's grant
authority and packages under the inviter's rights, so an invitation never grants more than its inviter could by hand.

```ts
const invitation = await iam.api.guests.invite(credential, {
  tenantId,
  email: 'gina@partner.example',
  sponsorId: aliceId,
  message: 'Welcome to the launch project',
  roleIds: [viewerRoleId],
  groupIds: [launchGroupId],
  accessDays: 30,
});
```

```ts title="Signature"
iam.api.guests.invite(
  credential: CredentialInput,
  input: GuestInviteInput,
): Promise<GuestInvitationView>
```

## list [#list]

Lists the tenant's guests by name, with their sponsor, access end, next review, and last sign-in.

**HTTP:** `POST /api/iam/guests/list` (requires a credential) · **Browser client:** `client.guests.list()`

* **Permission:** `iam:guests:read` on `iam/guests`.
* **Audited as:** `iam:guests:read`.
* **Errors:** `INVALID_INPUT` for a `status` other than `active`, `expired`, `removed`, or `converted`, a `limit`
  outside 1 to 1000, an `offset` outside 0 to 1000000, or an `expiringWithinDays` outside 0 to 3650.

Filter by `sponsorId`, by account `status` (an active guest whose access ended counts as `expired`), or by
`expiringWithinDays` (access ending at most that many days from now, ended ones included). `total` counts every match
before paging with `limit` (100 by default) and `offset`.

```ts title="Signature"
iam.api.guests.list(
  credential: CredentialInput,
  input: {
    tenantId: string;
    sponsorId?: string;
    status?: GuestAccountStatus;
    expiringWithinDays?: number;
    limit?: number;
    offset?: number;
  },
): Promise<GuestPage>
```

## listInvitations [#listinvitations]

Lists the tenant's guest invitations, newest first, without their tokens.

**HTTP:** `POST /api/iam/guests/listInvitations` (requires a credential) · **Browser client:** `client.guests.listInvitations()`

* **Permission:** `iam:guests:read` on `iam/guests/invitations`.
* **Audited as:** `iam:guests:read`.
* **Errors:** `INVALID_INPUT` for a `status` other than `pending`, `redeemed`, `revoked`, or `expired`.

A pending invitation past its lapse is reported as `expired` before the sweep job records it. Invitations revoked
because their inviter or sponsor was disabled or left carry `revokedReason` (`inviter-inactive` or
`sponsor-inactive`). Redeemed ones name the guest in `identityId`.

```ts title="Signature"
iam.api.guests.listInvitations(
  credential: CredentialInput,
  input: { tenantId: string; status?: GuestInvitationStatus },
): Promise<GuestInvitationView[]>
```

## mine [#mine]

Lists the active guests the caller sponsors, the soonest review first.

**HTTP:** `POST /api/iam/guests/mine` (requires a credential) · **Browser client:** `client.guests.mine()`

* **Permission:** None beyond a person's own session or API key in the tenant.
* **Audited as:** Not audited; it only reads.
* **Errors:** `ACCESS_DENIED` from a role session, session token, delegated session, or a session of another tenant.

It backs a "guests I sponsor" page where sponsors see each guest's review date and access end and renew them with
[`attest`](#attest). A person who sponsors nobody gets an empty list.

```ts title="Signature"
iam.api.guests.mine(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<GuestView[]>
```

## redeem [#redeem]

Redeems a guest invitation: creates the guest with the invited address and a password, grants what the invitation grants, and signs them in.

**HTTP:** `POST /api/iam/guests/redeem` (no credential) · **Browser client:** `client.guests.redeem()`

* **Permission:** None: public. The token from the invitation email is the proof.
* **Audited as:** `guest:redeem`, with the new guest as the actor and the invitation, inviter, sponsor, grants, and
  access end in the metadata.
* **Errors:** `INVITATION_INVALID` when the token is unknown, used, revoked, or lapsed, when the inviter is inactive or
  can no longer grant what it grants, or when the sponsor can no longer sponsor guests; `GUEST_NOT_ALLOWED` when the
  host's inbound or the home organization's outbound settings no longer admit the person; `TENANT_UNAVAILABLE` when the
  tenant or an ancestor is not active; `METHOD_NOT_ALLOWED` when the tenant does not accept password sign-in;
  `WEAK_PASSWORD` or `BREACHED_PASSWORD`; `IDENTITY_EXISTS` when an account with the address was created meanwhile;
  `LIMIT_EXCEEDED` at the tenant's member limit; `SOD_CONFLICT`; `INVARIANT_VIOLATION`; `RATE_LIMITED`;
  `INVALID_INPUT` without a token, name, or password.

The email counts as verified, because the invitation reached it. The guest's `expiresAt` is redemption plus the
invitation's `accessDays`, and its roles, groups, and packages end then too (a package's maximum duration can end its
assignment sooner). Birthright rules that test `identity.guest` apply right away. The result is the new public
identity, with its `guest` marker, plus either `{ token, session }` or an MFA challenge (`mfaRequired: true`) when the
tenant requires MFA. Over HTTP a response that issues a session sets the session cookie. Everything happens in one
transaction, so a failure leaves the invitation usable. Attempts are limited per tenant (500), client address (20),
and token (10) within the rate-limit window.

```ts
const result = await client.guests.redeem({ tenantId, token, name: 'Gina', password });
if ('mfaRequired' in result) {
  // enroll or verify the second factor, then continue
}
```

```ts title="Signature"
iam.api.guests.redeem(
  input: { tenantId: string; token: string; name: string; password: string },
): Promise<EnrollmentResult>
```

## remove [#remove]

Removes a guest: disables them and takes away everything they hold, in one transaction.

**HTTP:** `POST /api/iam/guests/remove` (requires a credential) · **Browser client:** `client.guests.remove()`

* **Permission:** `iam:guests:manage` on `iam/guests/{identityId}`, and a recent sign-in.
* **Audited as:** `iam:guests:manage`, plus `guest:remove` with the `reason`, the sponsor, and the counts.
* **Errors:** `INVALID_TRANSITION` (409) when the guest was already removed or converted; `INVALID_INPUT` without a
  `reason` (up to 512 characters), for the caller themselves, or for an owner (transfer ownership first);
  `RECENT_AUTH_REQUIRED`; `NOT_FOUND`.

The identity is disabled; its sessions, keys, and delegations end; every role binding, group and team membership,
package assignment, activation, and relationship it holds is removed, whichever authority granted it; its pending
requests and invitations are cancelled; its grant authorities are revoked; and the guest account closes as `removed`.
A guest whose access already ended can be removed too. The result is the guest with the counts of `sessions`,
`bindings`, `memberships`, and `packages` removed.

```ts title="Signature"
iam.api.guests.remove(
  credential: CredentialInput,
  input: { tenantId: string; identityId: string; reason: string },
): Promise<GuestRemoval>
```

## resendInvitation [#resendinvitation]

Sends a pending or lapsed guest invitation again with a new token and a new lifetime.

**HTTP:** `POST /api/iam/guests/resendInvitation` (requires a credential) · **Browser client:** `client.guests.resendInvitation()`

* **Permission:** `iam:guests:invite` on `iam/guests/invitations/{id}`.
* **Audited as:** `iam:guests:invite`, plus `guest:invitation-resend` with the new lapse.
* **Errors:** `INVALID_TRANSITION` (409) for a redeemed or revoked invitation; `CONFLICT` (409) when the address now
  belongs to someone in the tenant; `GUEST_NOT_ALLOWED` when the settings no longer admit it; `INVALID_SPONSOR` when
  the sponsor can no longer sponsor guests; `INVITATION_INVALID` when the original inviter is inactive or can no longer
  grant what the invitation grants; `DELIVERY_REQUIRED`; `TENANT_INACTIVE` when the tenant is not active;
  `INVALID_INPUT` for an `expiresInDays` outside 1 to 30; `NOT_FOUND`.

The earlier token stops working. The invitation keeps its inviter, sponsor, and grants, and only goes out when it
could be redeemed, since redemption checks the same things again. `expiresInDays` defaults to 14.

```ts title="Signature"
iam.api.guests.resendInvitation(
  credential: CredentialInput,
  input: { tenantId: string; invitationId: string; expiresInDays?: number },
): Promise<GuestInvitationView>
```

## revokeInvitation [#revokeinvitation]

Withdraws a pending guest invitation, so its token stops working.

**HTTP:** `POST /api/iam/guests/revokeInvitation` (requires a credential) · **Browser client:** `client.guests.revokeInvitation()`

* **Permission:** `iam:guests:manage` on `iam/guests/invitations/{id}`.
* **Audited as:** `iam:guests:manage`, plus `guest:invitation-revoke`.
* **Errors:** `INVALID_TRANSITION` (409) when the invitation is not pending; `NOT_FOUND` when it is not in this
  tenant.

```ts title="Signature"
iam.api.guests.revokeInvitation(
  credential: CredentialInput,
  input: { tenantId: string; invitationId: string },
): Promise<GuestInvitationView>
```

## setSponsor [#setsponsor]

Gives an active guest a new sponsor and clears a missing-sponsor flag.

**HTTP:** `POST /api/iam/guests/setSponsor` (requires a credential) · **Browser client:** `client.guests.setSponsor()`

* **Permission:** `iam:guests:manage` on `iam/guests/{identityId}`, and a recent sign-in.
* **Audited as:** `iam:guests:manage`, plus `guest:sponsor-change` with `from`, `to`, and `reason: 'administrator'`.
* **Errors:** `INVALID_SPONSOR` when the new sponsor is not an active member who is not a guest; `INVALID_TRANSITION`
  (409) when the guest account is no longer active (expired, removed, or converted); `RECENT_AUTH_REQUIRED`;
  `NOT_FOUND`.

Decisions see the new `principal.guestSponsorId` from the next request.

```ts title="Signature"
iam.api.guests.setSponsor(
  credential: CredentialInput,
  input: { tenantId: string; identityId: string; sponsorId: string },
): Promise<GuestView>
```
