Guests, contractors and family members
Applies to: SimpleTwo 0.9.x · Checked: 20.09.2026
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.
| Class | How it appears | Network profile | Directory |
|---|---|---|---|
employee | from the corporate directory at first sign-in (OIDC/ADFS) | ruleset rs-corp, gateway groups corp-egress and geo-pool | by role policy |
guest | invited by an employee holding guests:invite | ruleset rs-guest, geo-pool only | limited by role policy |
partner | created by an administrator | the same rs-guest, geo-pool only, but a separate role | limited by role policy |
| bot | by an administrator in the console | not applicable | by its capability list |
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}'
| Field | Required | Meaning |
|---|---|---|
username | yes | the guest's login; one any account already has (in any case) returns 409 |
phone | yes | the recovery channel, E.164, must not belong to another account |
display_name, email | no | how the guest is shown in lists |
password | no | your own (at least 8 characters) or generated by the server |
expires_at / ttl_hours | no | an exact date (RFC 3339) or a lifetime in hours |
Three behaviours people trip over:
- 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_takenif it is another guest's recovery number or an employee's phone in the directory (phone,mobileand 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. - 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.
- 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.
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:
| Call | What it does |
|---|---|
GET /v1/guests | my guests, those asking for help first |
POST /v1/guests/{login}/recovery-code | issues 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.
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
| Symptom | Likely cause |
|---|---|
403 on POST /v1/guests | the employee's role lacks guests:invite |
409 guest_taken: this username or phone number cannot be used for a new guest | the 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 colleagues | the address-book policy of their group — by design; check it with What does this person see |
| The guest connects but internal addresses are unreachable | expected: the guest ruleset has no corporate routes |
| Recovery "went through" but no call came | there will be no call: the code is issued by whoever invited them, or by an administrator |
404 you have no guest by that name | not your guest, or no such record — deliberately one answer for both |
409 this guest's access has expired | extend the lifetime first, then issue the code |