Skip to main content

Guests, contractors and family members

Applies to: SimpleTwo 0.9.x · Checked: 20.09.2026

Identity mode

Once the platform is switched to directory's token (platform_token: identity), a guest is an directory account: the invitation, the expiry, the card and the recovery code live there. The apps' doors are the same; see Guests and bots in directory.

The platform does not have one "user with checkboxes" but several access classes, and the class decides two things at once: what a person sees in the messaging and where they are let into the network. The split is arranged so that an invitation into a conversation never turns into access to systems.

ClassHow it appearsNetwork profileDirectory
employeefrom the corporate directory at first sign-in (OIDC/ADFS)ruleset rs-corp, gateway groups corp-egress and geo-poolby role policy
guestinvited by an employee holding guests:inviteruleset rs-guest, geo-pool onlylimited by role policy
partnercreated by an administratorthe same rs-guest, geo-pool only, but a separate rolelimited by role policy
botby an administrator in the consolenot applicableby its capability list
Why partner is a separate role rather than "a guest with rights"

Rights need to be extended for one group, not for everybody at once. While they are separate roles, changing contractor policy does not touch guests, and the other way round. Their rulesets are identical today, and that is a deliberate default rather than one entity under two names.

1. Who may invite​

Inviting is a role permission, not a role: guests:invite is granted to whichever role needs it (all of employee, or a separate "host" role).

You can check that an employee has it with their own token:

curl -fsS "https://s2.<domain>/v1/me/capabilities" -H "Authorization: Bearer $TOKEN"

guests:invite should be in the can list. Without it an invitation returns 403 — an answer, not a failure: the permission is granted in the console, in the role's properties.

2. Inviting​

An employee does it from the app (Contacts → invite a guest): a card appears with a QR code and credentials, and it can be sent to the guest by any channel. The same call from a shell:

curl -fsS -X POST "https://s2.<domain>/v1/guests" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"username":"ivanov-guest","display_name":"Ivan Ivanov","phone":"+79001234567","ttl_hours":720}'
FieldRequiredMeaning
usernameyesthe guest's login; one any account already has (in any case) returns 409
phoneyesthe recovery channel, E.164, must not belong to another account
display_name, emailnohow the guest is shown in lists
passwordnoyour own (at least 8 characters) or generated by the server
expires_at / ttl_hoursnoan exact date (RFC 3339) or a lifetime in hours

Three behaviours people trip over:

  1. The phone number is mandatory and unique. It is not for calling but for recovery: it is how the guest names themselves when asking for access back (§5). The number is compared as a number, not as text: "+7 912 345-67-89" and "89123456789" are one telephone. The invite gets 409 guest_taken if it is another guest's recovery number or an employee's phone in the directory (phone, mobile and the other phone fields). A login a person, guest, bot or room already has gets the same refusal — one for both fields, so an invite cannot be used to find out whose a number is.
  2. The default lifetime is 30 days, even if you never thought about it. The maximum is set by an administrator (a year by default), and a longer request is silently trimmed to it rather than refused.
  3. Who invited them stays in the record forever (invited_by), and the invitation is audited. A guest always belongs to somebody; an ownerless guest does not exist here. That field also decides who can restore their access (§5).

A guest's name, mail addresses, telephone numbers and attributes — like any account's — an administrator changes in their card: Directory → People → the guest → Profile → Edit. The first address and the first number of each list are the primary ones. An address or number another account or group holds is not given, and the directory names its holder.

3. The maximum invitation lifetime​

Set once, applying to every self-service invitation:

curl -fsS -X POST "https://s2.<domain>/admin/api/settings/guest-max-days" \
-H "Cookie: $ADMIN_SESSION" -H 'Content-Type: application/json' \
-d '{"days":90}'

Check: an invitation with ttl_hours above the limit is created, and the expires_at in the response comes back trimmed to the limit.

4. What a guest gets on the network — and what they do not​

A guest profile reaches the internet through a node of the geo-pool group and contains no corporate routes. The difference between "forbidden" and "not described" is essential here: a prohibition is lifted with a checkbox, and a route that does not exist cannot be lifted at all.

Hence the "employee's family member" scenario: a home device needs protected internet, and the guest class is enough for that. Nothing more needs to be created for them — and by the profile's construction, could not be.

Whom a guest sees in the directory is decided by the address-book policy of their group (see Groups — The global address list). A guest always sees the person who invited them, and the people they already share a conversation with as a name only.

5. Restoring access​

Access is restored by whoever invited them. That is the ordinary path, not a fallback: the guest presses "I cannot sign in", the host sees them under My guests and reads out a six-digit code — by voice, in a message, in person, whatever channel they already use. The guest sets their own password; the host never learns it.

Why not an SMS: an SMS proved possession of a number, and sending one is the telephony service's job — a role the platform does not have yet. A colleague who invited somebody last week recognises them, which is a different proof and, inside an organisation, a stronger one.

What the host sees under "My guests": their own guests and only theirs, with those asking for help at the top, each with a phone number and an expiry. Two buttons: Help sign in (issues a code) and Extend.

A code will not help an expired guest

An expired account is refused a code, and the server says why. Redeeming one is impossible anyway: confirmation refuses an inactive record. That is why extension is on the same screen: extend first, then issue the code.

The guest can start it themselves, from the app:

curl -fsS -X POST "https://s2.<domain>/v1/auth/recover" \
-H 'Content-Type: application/json' -d '{"phone":"+79001234567"}'

The answer is always the same — this is a request, not a check of the number. It marks the guest as "cannot sign in" and tells their host. The code and the new password go in a second call:

curl -fsS -X POST "https://s2.<domain>/v1/auth/recover/confirm" \
-H 'Content-Type: application/json' \
-d '{"phone":"+79001234567","code":"123456","new_password":"<new password>"}'

The limits are deliberate: one request per minute per number, a code that lives 10 minutes, five attempts, single use. Answers are identical for an existing and a non-existent number — otherwise enumerating numbers becomes a way to learn who you have.

An employee holding guests:invite has three calls, the same ones behind the "My guests" screen:

CallWhat it does
GET /v1/guestsmy guests, those asking for help first
POST /v1/guests/{login}/recovery-codeissues a code to me, to read out
POST /v1/guests/{login}/extend {"days":30}extends the lifetime

The scope is your own guests only. Somebody else's guest and a non-existent login answer identically (404): otherwise this call would be a way to enumerate logins. Issuing a code is audited with both names.

When the host is not around

A guest created in the console is recorded against the administrator who created them (invited_by), so such a guest appears under that administrator's "My guests" if they hold guests:invite. When the host is unavailable — on holiday, gone, without the permission — any administrator can issue the code: Users → the person's row → ⋯ → "Recovery code" (the users:write permission; issuing is audited). The button exists only for a local record with a phone number — the phone is how the person names themselves when redeeming it. The administrator does not learn the password either.

The Access recovery card on the same tab is a different thing: it shows requests guests have already filed, and it is hidden when there are none.

A corporate directory account is never issued a code: its password is changed by the directory.

6. Revocation​

  • By itself. The lifetime expires and access closes; a forgotten guest disappears with nobody's involvement. That is the reason the lifetime is mandatory. Sign-in closes the moment the lifetime ends, and within 15 minutes the system task "Block guests whose term ran out" blocks the account with an ordinary block: mail, files, calendar and VPN are suspended (data kept), and the console shows the guest as blocked. The task and each block are in "Service → System tasks".
  • Extending after the block. If the expired lifetime blocked the guest, an extension (the Extend button or a new date in the console) unblocks them. A guest blocked by hand is not unblocked by an extension — unblock them explicitly.
  • Early. Disable or delete the record in the console; sessions and devices are visible there by name. Network access closes at the device's next policy refresh, not "eventually".

Common refusals​

SymptomLikely cause
403 on POST /v1/gueststhe employee's role lacks guests:invite
409 guest_taken: this username or phone number cannot be used for a new guestthe login or the phone already belongs to another account — an employee, a guest or a bot; change the login or check the number
400 invalid expires_at (want RFC3339)the date is not in 2026-12-31T23:59:59Z form
The guest signs in but sees no colleaguesthe address-book policy of their group — by design; check it with What does this person see
The guest connects but internal addresses are unreachableexpected: the guest ruleset has no corporate routes
Recovery "went through" but no call camethere will be no call: the code is issued by whoever invited them, or by an administrator
404 you have no guest by that namenot your guest, or no such record — deliberately one answer for both
409 this guest's access has expiredextend the lifetime first, then issue the code