Aller à la documentation
API

Membres

Chaque opération de ce groupe : ce qu'elle accepte, ce qu'elle retourne et les erreurs qu'elle peut renvoyer.

Opérations

Who is in this workspace, and what each of them can reach.

A member is two grants and not one. A ROLE says what they may do; a set of ADDRESS grants says what they may do it to, each carrying member (read the address and send as it) or viewer, which only reads. Both are checked before mail goes out, so a screen showing one of them will explain the wrong refusal with great confidence.

implied: true means nobody chose the role. Sharing shipped long before roles did and a great many people still have address grants and no membership row; rather than lock them out until a backfill has run, the widest grant they hold names a built-in. A PATCH is what turns that inference into a decision, and until it happens, widening their addresses widens what they may do.

Everybody joins by invitation: POST /members invites, GET /members/invitations lists the invitations still waiting, and they can be sent again or withdrawn. A waiting invitation grants nothing until it is accepted. The owner is the first row of GET /members, and holds everything by definition.

GET/members

List members

Portéesmembers:readLit

Everybody with access to this workspace, the owner first and the rest by email. A page at a time: follow nextCursor while hasMore is true to read everybody.

Each row carries BOTH axes and a client must not collapse them. role is what somebody may do; addresses is what they may do it to, one entry per address with its own access. Someone with emails:send and an empty addresses may send from nothing, and someone holding every address under a viewer role may send from none of them either. The send path checks both, so a screen showing one of them will confidently explain the wrong refusal.

The one exception is addresses:all. Somebody whose permissions hold it reaches every address on every domain of the workspace, including ones added later, and sends as any of them when permissions also holds emails:send, whatever addresses and domains list. Those two still hold only direct grants, which such a member may have kept from before or been given since, or none at all. Read reach from permissions first, and from those arrays only when it lacks addresses:all.

Watch for implied: true. It means nobody chose that role: sharing shipped long before roles did, so a great many people have address grants and no membership row, and the widest grant they hold names a built-in rather than leaving them locked out until a backfill has run. A PATCH is what turns the inference into a decision.

The owner IS in this list, as the first row, marked isOwner: true. They are the thing the workspace is keyed on, they hold every permission by definition, and POST, PATCH and DELETE all refuse them with member_is_owner. Exclude isOwner when you are counting seats.

Requires the members:read scope.

Paramètres de requête

limitinteger

Rows per page, 1 to 100.

Au moins 1Au plus 100Par défaut25
cursorstring

The previous page's nextCursor, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 invalid_cursor.

Retourne

A page of people with access, by email.

Erreurs

Les erreurs que toute opération peut renvoyer400401403404422500Catalogue des erreurs

Aussi disponible dans

SDK
members.list()members.listAll()members.iterate()
CLI
openemail members list
MCP
listWorkspaceMembers

POST/members

Invite a member

Portéesmembers:writeModifie des données
Demande un code de vérification

Invites somebody to the workspace. Whether or not the address already has an OpenEmail account, the answer is an invitation and a 202, never a member: nobody is put into a workspace without accepting, and this endpoint is held to the same rule as the app.

The invitation carries the role, the addresses and the whole domains you name, and grants exactly those the moment it is accepted. Nothing is granted before that. An address invited in the last ten minutes answers invitation_too_soon, and asking twice refreshes the one invitation rather than sending two.

Somebody already in the workspace is refused with member_is_owner (the closest existing code). Change what an existing member may do with PATCH /members/{userId} and the address and domain grant calls, which only work on people already in.

invitedBy is left null deliberately. The column records which PERSON invited somebody, and a key is not a person.

Remember which axis this sets. A role does not reach an address, and the addresses do not widen the role. The one exception is a role holding addresses:all, which reaches every address with no ids at all.

The authority of a key is, in the same way, its own scopes intersected with the role it was issued under, so a role holding a permission the key lacks is refused with insufficient_authority, a 403. No key or token holds a console-only permission, so a role with one of those in it, addresses:all included, is handed out in the app.

Requires the members:write scope.

Corps de la requête

emailstringObligatoire

Who to invite. Lower-cased on the way in. It does not matter whether an OpenEmail account exists behind it: everybody is invited, and nobody is in the workspace until they accept.

Formatemail
roleIdstringObligatoire

From GET /roles. The owner role is refused: a workspace transfer is not this.

addressIdsstring[]

Addresses the invitation carries, all at the same access. They are granted the moment the person accepts, so "invite Sam as Support on help@" stays one intention.

Jusqu'à 64 éléments
domainIdsstring[]

Whole domains the invitation carries, at the same access. A domain grant reaches every address on it, including ones made later.

Jusqu'à 64 éléments
accessstring

member reads the address and sends as it; viewer only reads it. The second axis, ANDed with the role. This cannot widen what a role does not already allow.

L'un de"member""viewer"Par défaut"member"

Retourne

Invited. They are in the workspace once they accept.

Erreurs

403

The key lacks the scope, or may not send as that address.

step_up_required: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with POST /security/step-up, send it to POST /security/step-up/verify, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.

404

role_not_found for the roleId, or an addressIds / domainIds entry that is not on this workspace.

409

invitation_too_soon (that address was invited in the last ten minutes) or invitation_limit_reached.

422

member_is_owner: the address is already in this workspace, or is the owner. Use PATCH to change what an existing member may do.

Les erreurs que toute opération peut renvoyer400401500Catalogue des erreurs

Aussi disponible dans

SDK
members.add()
CLI
openemail members add
MCP
inviteMember

GET/members/{userId}

Retrieve a member

Portéesmembers:readLit

Read off the same union the list computes, rather than by a query of its own. A member is the union of two populations (a membership row and a set of address grants, either of which exists perfectly well without the other), and a single-row lookup would be a second implementation of that union whose blind spot is precisely the legacy grant holders, who are most of the table today.

Requires the members:read scope.

Paramètres de chemin

userIdstringObligatoire

The ACCOUNT id, which is what GET /members returns and not the email address. An email identifies a person to a human and is the wrong key here: it can be changed on the account, and a caller holding a stale one would quietly address the wrong grant.

Retourne

The member, with their role and their addresses.

Erreurs

404

No such member on this workspace.

Les erreurs que toute opération peut renvoyer400401403422500Catalogue des erreurs

Aussi disponible dans

SDK
members.get()
CLI
openemail members get
MCP
listWorkspaceMembers

PATCH/members/{userId}

Change a member's role

Portéesmembers:writeModifie des données
Demande un code de vérification

Changes the role and nothing else, for somebody ALREADY in the workspace. It is also how a legacy grant holder stops being implied: they have address or domain grants and no membership row, this writes one, and from then on their permissions are what somebody chose rather than what their access happened to imply. Their grants are untouched.

A role holding a permission the key lacks is refused with insufficient_authority, a 403. No key or token holds a console-only permission, so moving somebody onto a role with addresses:all in it, which would reach every address for them without a single grant, is done in the app.

Addresses are deliberately not patchable from here. They are a set with a per-element access level, and an array on a PATCH would have to mean replace-whole: a silent mass revocation every time a client sends a list it read five minutes ago. The two address endpoints below move one at a time, so what happened is always what was asked for.

The owner role cannot be handed out. Making somebody an owner is a workspace transfer, which has entirely different consequences and is not an operation this API has.

Requires the members:write scope.

Paramètres de chemin

userIdstringObligatoire

The ACCOUNT id, which is what GET /members returns and not the email address. An email identifies a person to a human and is the wrong key here: it can be changed on the account, and a caller holding a stale one would quietly address the wrong grant.

Corps de la requête

roleIdstringObligatoire

The role to move them to. Their addresses are untouched by this.

Retourne

Saved. implied is false from here on.

Erreurs

403

The key lacks the scope, or may not send as that address.

step_up_required: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with POST /security/step-up, send it to POST /security/step-up/verify, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.

409

role_immutable: the owner role cannot be handed out.

422

member_is_owner, or member_not_found when the account is not in this workspace yet. Nobody joins through PATCH: invite them with POST /members and they are in once they accept.

Les erreurs que toute opération peut renvoyer400401404500Catalogue des erreurs

Aussi disponible dans

SDK
members.update()
CLI
openemail members update
MCP
setMemberRole

DELETE/members/{userId}

Remove a member

Portéesmembers:writeSupprime
Demande un code de vérification

Takes somebody out of the workspace entirely: the membership row AND every address grant they hold here. Removing only the first would be the worst of both worlds. They would vanish from the list and go on reading the mail.

Idempotent, and it does NOT 404 on somebody who is not a member. The workspace owner is the one id it refuses, with 422 member_is_owner. That is not laxness about ids: the population this endpoint most needs to reach is the legacy grant holders, who have address grants and no membership row at all, so a pre-flight existence check would refuse exactly the people whose access most wants revoking. addressesRevoked reports what actually happened.

It does not touch their account, their sent mail or anything they wrote. It removes their access to this workspace.

Requires the members:write scope.

Paramètres de chemin

userIdstringObligatoire

The ACCOUNT id, which is what GET /members returns and not the email address. An email identifies a person to a human and is the wrong key here: it can be changed on the account, and a caller holding a stale one would quietly address the wrong grant.

Retourne

200object

Removed, with the number of address grants that went with them.

objectstring
L'un de"member"
userIdstring
deletedboolean
L'un detrue
addressesRevokedinteger

Grants actually removed. Zero is the honest report of a no-op.

Erreurs

403

The key lacks the scope, or may not send as that address.

step_up_required: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with POST /security/step-up, send it to POST /security/step-up/verify, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.

Les erreurs que toute opération peut renvoyer400401404422500Catalogue des erreurs

Aussi disponible dans

SDK
members.remove()
CLI
openemail members remove
MCP
removeMember

POST/members/{userId}/addresses

Grant an address

Portéesmembers:writeModifie des données
Demande un code de vérification

Hands one person one address, or changes what they may do with one they already have. An upsert: there is one row per address-and-person pair, so re-posting with a different access promotes a viewer to a member rather than adding a second grant.

A POST to the sub-collection rather than a PUT on the pair, because the row has no id a caller ever names.

This is the SECOND axis and it cannot widen the first: access: "member" on somebody whose role lacks emails:send does not let them send, it lets them read. Both have to agree before a message goes out.

Gated on members:write rather than on owning the address, which is where it differs from the older sharing path in the console. Ownership is the right gate for the person who put the domain in and the wrong one for an admin who owns nothing and is running the workspace access on behalf of the owner. Both write the same row.

The whole member comes back rather than the grant alone, so a client can re-render the row it is looking at without a second request, and so the answer reads the same whether the grant was new or an amendment.

Requires the members:write scope.

Paramètres de chemin

userIdstringObligatoire

The ACCOUNT id, which is what GET /members returns and not the email address. An email identifies a person to a human and is the wrong key here: it can be changed on the account, and a caller holding a stale one would quietly address the wrong grant.

Corps de la requête

addressIdstringObligatoire

An address on this workspace.

accessstring

member reads the address and sends as it; viewer only reads it. The second axis, ANDed with the role. This cannot widen what a role does not already allow.

L'un de"member""viewer"Par défaut"member"

Retourne

The whole member, with the grant applied.

Erreurs

403

The key lacks the scope, or may not send as that address.

step_up_required: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with POST /security/step-up, send it to POST /security/step-up/verify, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.

Les erreurs que toute opération peut renvoyer400401404422500Catalogue des erreurs

Aussi disponible dans

SDK
members.grantAddress()
CLI
openemail members grant-address
MCP
grantMemberAddress

DELETE/members/{userId}/addresses/{addressId}

Revoke an address

Portéesmembers:writeSupprime
Demande un code de vérification

Takes one address back and leaves the person in the workspace, with their role and their other addresses. The narrow revocation, and the one to reach for when somebody moves team.

An address that is not on this workspace is refused rather than quietly ignored: a typo in the id reporting a successful revocation that never happened is the exact failure this endpoint exists to prevent.

The member comes back rather than a tombstone, because the interesting answer is what they can still reach. A { deleted: true } here would leave a client to work that out by subtraction.

Requires the members:write scope.

Paramètres de chemin

userIdstringObligatoire

The ACCOUNT id, which is what GET /members returns and not the email address. An email identifies a person to a human and is the wrong key here: it can be changed on the account, and a caller holding a stale one would quietly address the wrong grant.

addressIdstringObligatoire

An address on THIS workspace. One belonging to another is refused, not ignored.

Retourne

The member, with what they can still reach.

Erreurs

403

The key lacks the scope, or may not send as that address.

step_up_required: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with POST /security/step-up, send it to POST /security/step-up/verify, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.

Les erreurs que toute opération peut renvoyer400401404422500Catalogue des erreurs

Aussi disponible dans

SDK
members.revokeAddress()
CLI
openemail members revoke-address
MCP
revokeMemberAddress

GET/members/invitations

List pending invitations

Portéesmembers:readLit

The invitations to this workspace that are still waiting, by email: the role and the addresses and whole domains each will grant once accepted, when it expires, and whether the last email reached them. A page at a time: follow nextCursor while hasMore is true.

A waiting invitation grants nothing. It becomes access only at the moment somebody accepts it, which is why it is listed here rather than in GET /members. An expired one stays on this list with expired: true until it is sent again or withdrawn.

A key limited to particular addresses or domains lists only the invitations that grant nothing outside them.

Requires the members:read scope.

Paramètres de requête

limitinteger

Rows per page, 1 to 100.

Au moins 1Au plus 100Par défaut25
cursorstring

The previous page's nextCursor, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 invalid_cursor.

Retourne

A page of invitations nobody has accepted yet, by email.

Erreurs

Les erreurs que toute opération peut renvoyer400401403404422500Catalogue des erreurs

Aussi disponible dans

SDK
members.listInvitations()members.listAllInvitations()members.iterateInvitations()
CLI
openemail members list-invitations
MCP
listInvitations

DELETE/members/invitations/{invitationId}

Withdraw an invitation

Portéesmembers:writeSupprime

Withdraws an invitation nobody has accepted. Its link stops working at once and nothing it would have granted is granted. Inviting the same address later with POST /members sends a new one.

A key limited to particular addresses or domains can withdraw only an invitation that grants nothing outside them. Any other answers 404.

Requires the members:write scope.

Paramètres de chemin

invitationIdstringObligatoire

The invitation id from GET /members/invitations, winv_ and 24 hex.

Retourne

200object

Withdrawn. The link no longer works.

objectstring
L'un de"invitation"
idstring
emailstring
revokedboolean
L'un detrue

Erreurs

404

No invitation with that id is waiting: it is unknown, or already withdrawn.

409

invitation_accepted: it was accepted first. Remove the member instead.

Les erreurs que toute opération peut renvoyer400401403422500Catalogue des erreurs

Aussi disponible dans

SDK
members.revokeInvitation()
CLI
openemail members revoke-invitation
MCP
revokeInvitation

POST/members/invitations/{invitationId}/resend

Send an invitation again

Portéesmembers:writeEnvoie des e-mails

Sends a waiting invitation again: a new link, fourteen more days, and the old link retired, so only the newest email works. It renews an expired invitation too. delivered and deliveryError describe this send.

The per-person daily allowance is counted against the person who sent the invitation first when a key asks, and against the person an app acts for when an app does.

The invitation's role cannot hold more than the caller does, the same rule as POST /members: a role with a permission the key or token lacks, any console-only one included, is insufficient_authority, a 403. An app acting for a member is also refused with insufficient_authority when the invitation carries addresses or domains that member does not reach.

Requires the members:write scope.

Paramètres de chemin

invitationIdstringObligatoire

The invitation id from GET /members/invitations, winv_ and 24 hex.

Retourne

Sent again, with a new link and a new expiry.

Erreurs

403

insufficient_authority: the invitation's role holds a permission the caller does not, or an app acting for a member would send again addresses or domains that member does not reach.

404

No invitation with that id is waiting.

409

invitation_too_soon (that address was sent an invitation in the last ten minutes) or invitation_limit_reached.

Les erreurs que toute opération peut renvoyer400401422500Catalogue des erreurs

Aussi disponible dans

SDK
members.resendInvitation()
CLI
openemail members resend-invitation
MCP
resendInvitation

POST/members/{userId}/domains

Grant a domain

Portéesmembers:writeModifie des données
Demande un code de vérification

Hands one person a whole domain: every address on it, including addresses added after the grant. An upsert, like POST /members/{userId}/addresses: posting again with a different access changes the grant rather than adding a second one.

It is the address axis, so it cannot widen the role: access: "member" lets somebody send only if their role also holds emails:send. Somebody who is not in the workspace yet is refused, so invite them first, and the owner is refused because they already reach every domain. A key or an app limited to particular addresses or domains is refused, and an app acting for a member can only give a domain that member reaches. Adding people needs a plan with team access, so a workspace without it is refused with 403 plan_required.

Requires the members:write scope.

Paramètres de chemin

userIdstringObligatoire

The ACCOUNT id, which is what GET /members returns and not the email address. An email identifies a person to a human and is the wrong key here: it can be changed on the account, and a caller holding a stale one would quietly address the wrong grant.

Corps de la requête

domainIdstringObligatoire

A domain on this workspace.

accessstring

member reads the address and sends as it; viewer only reads it. The second axis, ANDed with the role. This cannot widen what a role does not already allow.

L'un de"member""viewer"Par défaut"member"

Retourne

The whole member, with the grant applied.

Erreurs

403

The key lacks the scope, or may not send as that address.

step_up_required: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with POST /security/step-up, send it to POST /security/step-up/verify, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.

Les erreurs que toute opération peut renvoyer400401404422500Catalogue des erreurs

Aussi disponible dans

SDK
members.grantDomain()
CLI
openemail members grant-domain
MCP
grantMemberDomain

DELETE/members/{userId}/domains/{domainId}

Revoke a domain

Portéesmembers:writeSupprime
Demande un code de vérification

Takes a whole domain back and leaves the person in the workspace, with their role and their other grants. Addresses on the domain that were granted one by one stay granted. A domain that is not on this workspace is refused rather than quietly ignored, and the member comes back so the answer shows what they can still reach.

Requires the members:write scope.

Paramètres de chemin

userIdstringObligatoire

The ACCOUNT id, which is what GET /members returns and not the email address. An email identifies a person to a human and is the wrong key here: it can be changed on the account, and a caller holding a stale one would quietly address the wrong grant.

domainIdstringObligatoire

A domain on THIS workspace, as GET /domains returns it. One belonging to another is refused, not ignored.

Retourne

The member, with what they can still reach.

Erreurs

403

The key lacks the scope, or may not send as that address.

step_up_required: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with POST /security/step-up, send it to POST /security/step-up/verify, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.

Les erreurs que toute opération peut renvoyer400401404422500Catalogue des erreurs

Aussi disponible dans

SDK
members.revokeDomain()
CLI
openemail members revoke-domain
MCP
revokeMemberDomain

Objets

Invitationobject

objectstring
L'un de"invitation"
idstring
emailstring

Lower-cased. The only address that can accept it.

roleobject
idstring
namestring
builtinstring
Peut être nullL'un de"owner""admin""member""viewer""developer""billing"
addressesobject[]
addressIdstring
addressstring
accessstring
L'un de"member""viewer"
domainsobject[]
domainIdstring
domainstring
accessstring
L'un de"member""viewer"
expiresAtstring
Formatdate-time
expiredboolean

The link no longer works. An expired invitation stays waiting until it is sent again, which renews it, or withdrawn.

lastSentAtstring

When the invitation email last went out, the first send included.

Formatdate-time
deliveredboolean

Whether the invitation email left the mail server. False is worth surfacing: nothing reached them. Null when no outcome was recorded for the last send.

Peut être null
deliveryErrorstring
Peut être null
createdAtstring
Formatdate-time

InvitationListobject

objectstring
L'un de"list"
hasMoreboolean

True when another page follows. Pass nextCursor back as cursor to read it.

nextCursorstring

An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

Peut être null

Memberobject

objectstring
L'un de"member"
userIdstring

The account. This is what every path here takes, not the email address.

emailstring
namestring
Peut être null
imagestring
Peut être null
roleobject

The role they hold. id is null when nobody chose it. See implied.

idstring
Peut être null
namestring
builtinstring
Peut être nullL'un de"owner""admin""member""viewer""developer""billing"
isOwnerboolean

True on exactly one row, the account the workspace is keyed on. They sort first, hold every permission whatever their role row says, and POST, PATCH and DELETE all refuse them with member_is_owner. Exclude them when counting seats.

impliedboolean

NOBODY CHOSE THIS ROLE. Sharing shipped long before roles did, so a great many people have address grants and no membership row at all; rather than deny them their mail until a backfill has run, the widest grant they hold names a built-in and that is what is reported. Show it as implied by access rather than as a decision. Until somebody PATCHes them, widening their addresses silently widens what they may do.

permissionsstring[]

The resolved list, the same one role.permissions would give.

L'un de"emails:send""emails:read""drafts:read""drafts:write""threads:read""threads:write""files:read""files:write""labels:read""labels:write""contacts:read""contacts:write""audiences:read""audiences:write""calendar:read""calendar:write""templates:read""templates:write""domains:read""domains:write""webhooks:read""webhooks:write""rules:read""rules:write""connections:read""members:read""members:write""roles:read""roles:write""settings:read""settings:write""keys:write""keys:read""keys:manage""forms:read""forms:write""billing:read""billing:write""account:read""account:write""addresses:all""api-keys:read""api-keys:write""workspace:manage"
addressesobject[]

Which addresses on this workspace they were granted, and how. Empty is a real answer and a common one: somebody with a role and no addresses can do a great deal to nothing at all. The exception is permissions holding addresses:all, which reaches every address on every domain of the workspace, including ones added later, whatever this lists. It still lists only the addresses granted directly, which such a member may have kept from before or been given since.

addressIdstring
addressstring
accessstring

The older per-address vocabulary, deliberately not overlapping with the permission names: member reads the address and sends as it, viewer only reads it. ANDed with the role rather than added to it. viewer here refuses a send from somebody whose role holds emails:send, unless the role also holds addresses:all.

L'un de"member""viewer"
domainsobject[]

Whole domains they hold. A domain grant reaches every address on that domain, including ones created after the grant, at the given access. Addresses under a held domain also appear in addresses only when they were granted individually as well. Somebody whose permissions hold addresses:all reaches every domain whether or not one is listed here.

domainIdstring
domainstring
accessstring
L'un de"member""viewer"
createdAtstring

When they were made a member. Null for the legacy grant holders, who never were.

Peut être nullFormatdate-time

MemberListobject

objectstring
L'un de"list"
hasMoreboolean

True when another page follows. Pass nextCursor back as cursor to read it.

nextCursorstring

An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

Peut être null