---
title: "Members"
description: "`members->list`, `listAll`, `iterate`, `get`, `add`, `update`, `remove`, `grantAddress`, `revokeAddress`, `grantDomain`, `revokeDomain` and the invitation methods beside them."
url: "https://openemail.uk/docs/php/members"
area: "PHP"
category: "Mailbox"
---

# Members

`members->list`, `listAll`, `iterate`, `get`, `add`, `update`, `remove`, `grantAddress`, `revokeAddress`, `grantDomain`, `revokeDomain` and the invitation methods beside them.

## Every method

**members.php**

```
$supportRoleId = 'role_8b1f4c2e9a7d3b60e5f1a2c4';
$viewerRoleId = 'role_2c7e9a1f4b8d3e60c5a7f1b9';

$invitation = $client->members->add([
    'email' => 'sam@acme.com',
    'roleId' => $supportRoleId,
    'addressIds' => ['2b81de07-9c3f-4a61-b8e2-5d07f4c19a36'],
    'access' => 'member',
]);
echo $invitation['id'], ' ', $invitation['expiresAt'], PHP_EOL;

foreach ($client->members->listAll() as $person) {
    echo $person['email'], ' ', $person['userId'], PHP_EOL;
}

$samId = 'q7Vd3kX9mT2pLw8RzN4bYc6HfJ1sGa5E';
$member = $client->members->get($samId);
echo $member['role']['name'], $member['implied'] ? ' (implied)' : '', PHP_EOL;

$client->members->update($samId, ['roleId' => $viewerRoleId]);

$addressId = 'c40a95f2-1e7b-4d38-a6c9-82f05b3d7e14';
$client->members->grantAddress($samId, ['addressId' => $addressId, 'access' => 'viewer']);
$client->members->revokeAddress($samId, $addressId);

$removed = $client->members->remove($samId);
echo $removed['addressesRevoked'], PHP_EOL;
```

Two grants per person, and they must not be collapsed. `role` is what they may do. `addresses` and `domains` are what they may do it to. Both have to agree: a role holding `emails:send` with `access` set to `viewer` on invoices@ is somebody who may send mail and may not send it from that address. The exception is a role holding `addresses:all`, which reaches every address whatever `addresses` lists, because that array holds only direct grants. So check `permissions` before reading `addresses` as the whole of somebody’s reach.

Every call about one person takes their `userId` as its first argument, not their email, so read it off `list` or `listAll` as the sample does. `add` is the one exception, because it invites an address: the person has a `userId` only once they accept, and `listInvitations` follows the invitation until then. The body of `add`, `update`, `grantAddress` and `grantDomain` is one array under the API’s camelCase names (`roleId`, `addressIds`, `addressId`).

`list` returns one `OpenEmail\Result\Page`, `listAll` returns every member in one array, and `iterate` returns a `Generator` that yields one member at a time. A member comes back as an array keyed in camelCase, and `role` is an array inside it, so `$member['role']['name']` reads the role’s name.

> `implied` true means nobody chose the role. They hold addresses or domains and no role row, so it was inferred from the widest grant they have. Treat it as “not decided yet”, and `update` is what turns the inference into a decision. Until then, widening their address access silently widens what they may do.

> The workspace owner is the first row, with `isOwner` true, while `add`, `update` and `remove` still refuse them with `member_is_owner`, a 422 thrown as a `ValidationException`. An unshared workspace reports one member rather than none, so exclude `isOwner` when you are counting seats: `count(array_filter($client->members->listAll(), fn(array $member): bool => !$member['isOwner']))`.

> `remove` takes both axes, the role AND every address and domain grant on this workspace, and reports how many grants it revoked, domains included, in `addressesRevoked`. `revokeAddress` is the narrow one, for somebody who moved team rather than somebody who left, and `revokeDomain` does the same for a whole domain.

> `add`, `update`, `remove` and the four grant and revoke calls ask an OAuth access token for a verification code, and an API key never. Until the token has one they throw a `PermissionException` whose `isStepUpRequired()` is true. `add` and `grantDomain` also need a plan that includes a team, and on one that does not they throw a `PermissionException` with `errorCode` set to `plan_required`.

## Parameters

- `email` (string, required): Who to invite, trimmed and lower-cased. It does not need an account yet: everybody is invited, and the role and grants land when they accept. Somebody already in the workspace is `member_is_owner` (422). An address that signs in with a password of its own cannot be invited and comes back `mailbox_login` (403), so invite the person’s own email instead.
- `roleId` (string, required): The role they will hold, 1 to 128 characters, and it must be a role on this workspace: an unknown id is `role_not_found` (404), thrown as a `NotFoundException`. The owner role cannot be handed out and comes back `role_immutable` (409), because making somebody an owner is a workspace transfer and there is no call for that here.
- `addressIds` (array): Addresses the invitation carries, at most 64 ids of 1 to 128 characters each, granted when it is accepted. Every id is checked before anything is written, so one that is not an address on this workspace refuses the whole call with 422 `member_not_found` and nothing is sent. Inviting the same address again within ten minutes is 409 `invitation_too_soon`.
- `domainIds` (array): Whole domains the invitation carries, at most 64 ids, granted when it is accepted. A domain grant reaches every address on that domain, including ones made later. An id that is not a domain on this workspace is 422 `member_not_found`, like an address.
- `access` (string): What they may do with every id in `addressIds` and `domainIds`: `member` reads the address and sends as it, `viewer` only reads it. Defaults to `member`, the level the console and the older sharing path have always used, so the same call means the same thing from a script and from a screen. Grant a mixture by calling `grantAddress` afterwards for the ones that differ.

## Response

- `object` (string): Always `member`. A removal answers with the same value, their `userId`, `deleted` set to true and `addressesRevoked`, and none of the other fields below.
- `userId` (string): Their account id, and the handle every other member call takes as its first argument: `get`, `update`, `remove` and the four grant and revoke calls. Adding somebody is the one call that works from an email instead, because whoever is adding a colleague knows their address and not their id.
- `email` (string): The email on their account, echoed as that row stores it. This resource never writes it, and the lower-casing on `add` applies to the address you send for the lookup rather than to what comes back. After the owner, the members list is sorted by it rather than by when people joined, because the list is read to find one person rather than to see what changed.
- `name` (string or null): Their display name, taken from their account, where the column always holds a value. The null in the type is defensive rather than a state this API has been seen to produce. It belongs to them rather than to the workspace, so nothing on this resource can set it.
- `image` (string or null): Their avatar, taken from their account, and null when they have not set one.
- `role.id` (string or null): The id of the role they hold, read as `$member['role']['id']`, or null when nobody chose it. See `implied`. A null here is the one case where `role` reports an inference rather than a decision somebody made.
- `role.name` (string): The role’s name. For an implied member it is the name of the built-in template their access maps to, not a row on this workspace.
- `role.builtin` (string or null): Which built-in the role is, `owner`, `admin`, `member`, `viewer`, `developer` or `billing`, or null for a custom one. `owner` appears only on the owner’s own row, alongside `isOwner` true. Assigning that role to anybody is refused with `role_immutable` (409).
- `isOwner` (bool): True on exactly one row, the account the workspace is keyed on. They hold every permission whatever their role row says, they sort first, and `add`, `update` and `remove` all refuse them with `member_is_owner`. Exclude them when you are counting seats.
- `implied` (bool): True when this person has address or domain grants and no member row, so their role was inferred rather than chosen: any grant of `member` makes it the built-in Member, otherwise Viewer. Never true for the owner. Show it as "implied by access". Until an `update` turns the inference into a decision, widening their address access silently widens what they may do.
- `permissions` (array): The role’s permissions flattened onto the member, so one read answers "may they?" without fetching the role. For an implied member they come from the built-in TEMPLATE rather than from this workspace’s role row, so editing the built-in Member role does not change what an implied member holds.
- `addresses` (array): The addresses they have been given, sorted by address, one array each with its own access level. Empty for somebody holding a role and no grants, which is what a new member looks like until an address is granted, and is the right failure to have while you are still deciding what they should see.
- `addresses[].addressId` (string): The id of the address, and what `grantAddress` and `revokeAddress` take. An id that is not an address on this workspace is refused on both, rather than reporting a revocation that never happened.
- `addresses[].address` (string): The full address, lower-cased, rebuilt from its local part and its domain.
- `addresses[].access` (string): What they may do with this one address: `member` reads it and sends as it, `viewer` only reads it. This and the role both have to allow a send before one happens, so a role holding `emails:send` over a `viewer` grant sends from nothing. The stored column is called `role`, and it is renamed here so one array does not carry two `role` keys drawn from two vocabularies.
- `domains` (array): The whole domains they have been given, each with `domainId`, `domain` and `access`. A domain grant reaches every address on it, including ones made later, so read it beside `addresses` before deciding somebody cannot reach an address. `grantDomain` and `revokeDomain` take the `domainId`.
- `createdAt` (string or null): When their member row was written, as an ISO 8601 string, and null when there is no member row at all. That null describes the same population as `implied` true: people who hold grants from before roles existed and whom nobody has since given a role.

## Invitations

**invitations.php**

```
$waiting = $client->members->listAllInvitations();

foreach ($waiting as $invitation) {
    if ($invitation['expired']) {
        $client->members->resendInvitation($invitation['id']);
    }
}

$client->members->revokeInvitation('winv_6bb640f5b99e47deb758f1f5');
```

`add` answers with an invitation, and these are the calls that follow one up. `listInvitations` returns one `OpenEmail\Result\Page` of the ones nobody has accepted yet, `listAllInvitations` returns all of them in one array, and `iterateInvitations` returns a `Generator` that yields one at a time. `resendInvitation` sends one again with a new link and fourteen more days, and `revokeInvitation` withdraws it. A waiting invitation grants nothing until it is accepted.

An invitation is an array with `id`, `email`, `role`, `addresses`, `domains`, `expiresAt`, `expired`, `lastSentAt` and `createdAt`. `delivered` and `deliveryError` report how the last invitation email fared, so a script can tell an invitation that was written from one that reached somebody.

> `resendInvitation` refuses the same address twice within ten minutes with 409 `invitation_too_soon`, and `revokeInvitation` refuses one that was accepted first with 409 `invitation_accepted`. Both are thrown as a `ConflictException`, so `isConflict()` is true and `errorCode` tells them apart.
