Discovery and device sessions
Questi contenuti non sono ancora disponibili nella tua lingua.
How a native client, such as the GuestCraft mobile app or a branded build of it, finds an instance and signs a person in (ADR 0041). The flow is OAuth 2.0 for native apps (RFC 8252): the person signs in on the instance’s own page in the system browser, and the app exchanges a one-time code with PKCE S256 (RFC 7636) for a device token of its own. API revision 3.4.0; since 4.6.0 a signed-in browser can also pair a phone with a QR code.
Discover the instance
Section titled “Discover the instance”GET /.well-known/guestcraftPublic, no credentials, Cache-Control: public, max-age=300. The same document is
GET /api/v1/instance (instance.get).
{ "name": "Hotel Aurora", "api_base_url": "https://pms.example.com/api/v1", "server_version": "0.1.0", "api_revision": "3.4.0", "min_app_version": "0.1.0", "capabilities": ["front_desk", "housekeeping", "mobile_sessions", "…"], "auth": { "methods": ["password"], "mobile": { "authorization_url": "https://pms.example.com/mobile/authorize", "token_url": "https://pms.example.com/api/v1/device_sessions", "redirect_schemes": ["dev.guestcraft.app"] } }}A client should refuse an instance whose URLs are on another origin, that lacks the
mobile_sessions capability, that does not list its redirect scheme, or whose min_app_version is
above its own version; and ignore members it does not know. The SDK’s discoverInstance(baseUrl)
reads and validates the document. An operator sets the name with GUESTCRAFT_INSTANCE_NAME and the
allowed schemes with GUESTCRAFT_MOBILE_APP_SCHEMES (default dev.guestcraft.app; add exp to
develop with Expo Go).
Sign in through the browser
Section titled “Sign in through the browser”-
Create a verifier of 43 to 128 unreserved characters, its challenge
BASE64URL(SHA256(verifier))without padding, and astateof 8 to 256 unreserved characters. -
Open
authorization_urlin the system browser (ASWebAuthenticationSession, Custom Tabs), never in an embedded web view:https://pms.example.com/mobile/authorize?redirect_uri=dev.guestcraft.app://auth&code_challenge=…&code_challenge_method=S256&state=…&device_name=Pixel%209The page refuses a redirect URI with another scheme, a missing or malformed challenge, another method than
S256, and a device name that is empty, longer than 100 characters or holds control characters; it then shows an error and redirects nowhere. Otherwise the person signs in if needed, sees the device name, their organization and the instance’s address, and chooses:- Allow redirects to
redirect_uri?code=gcc_…&state=…. The code is valid for two minutes and bound to the person, their active organization, the challenge, the redirect URI and the device name. The page issues it withPOST /api/v1/device_authorizations, which only a browser session from the instance’s origin may call. - Cancel redirects to
redirect_uri?error=access_denied&state=….
- Allow redirects to
-
Check that
stateis the one you sent, then exchange the code. The request needs no cookie, Origin or other credential:POST /api/v1/device_sessionsContent-Type: application/json{ "code": "gcc_…", "code_verifier": "…", "redirect_uri": "dev.guestcraft.app://auth" }{"token": "gcd_…","device_session": {"id": "dvs_…", "name": "Pixel 9", "created_at": "…", "last_used_at": "…","current": true, "etag": "\"1761724800000\""},"user": { "id": "usr_…", "name": "Ada Rossi", "email": "ada@example.com" }}Each code serves one attempt. An unknown, expired or already used code, another redirect URI and a wrong verifier all answer the same
400 device_session.invalid_grant; the code cannot be tried again, and a second use of a code also revokes the session the first one created. Like a pairing code below, the exchange is admitted first and can answer429 auth.rate_limitedwithRetry-After, without using the code.
Keep the token in the platform keystore. It is shown once; the instance stores only its SHA-256.
Pair a phone with a QR code
Section titled “Pair a phone with a QR code”A person signed in on the web pairs a phone without a second sign-in: the browser shows a QR code
with a one-time pairing code, and the app that scans it gets its own device session, as that
person. Discovery lists the mobile_pairing capability when the instance offers it.
-
The browser issues a pairing. It needs its session cookie and the instance’s Origin; it sends no body:
POST /api/v1/device_pairings{"id": "dvp_…", "status": "pending", "expires_at": "2026-10-31T09:02:00Z","device_session": null, "etag": "\"1793437200000\"","code": "gcp_…", "expires_in": 120,"pairing_url": "https://pms.example.com/mobile/pair#gcp_…"}The QR code carries
pairing_url: the instance’s own address, with the code in the fragment, which a browser that opens the address never sends to a server. The code is shown in this answer only, is valid for two minutes, and is exchanged once, only while this browser session exists.expires_inis the whole seconds the code has left when the answer is sent, by the instance’s clock (RFC 8628): count down from it instead of comparingexpires_atwith the computer’s clock. An issuance that outlasted the code answers0andstatus: expired. -
The app scans the code, takes the origin of
pairing_url, reads discovery there, refuses an instance withoutmobile_pairing, shows the instance, and sends the code with a name of its own to discovery’stoken_urlonly, without cookies or Origin:POST /api/v1/device_sessionsContent-Type: application/json{ "pairing_code": "gcp_…", "device_name": "Pixel 9" }The answer is the same
201as for a sign-in code: the token, the device session and the person. An unknown, expired, used or withdrawn code, a sign-in code, a code whose browser has signed out, and a code issued while the instance had another base URL all answer400 device_session.invalid_grant, and a second use also revokes the session the first one created. A name that is empty, longer than 100 characters or holds control or invisible characters answers400 device_session.invalid_requestwithfield: device_name, and the code stays usable.The code is bound to the instance’s origin: it is exchanged only at the origin of
pairing_url. Sent to another address that reaches the same instance, it answers the same400 device_session.invalid_grantbefore it is looked up, and stays usable at the right one. The instance reads the address the request reached, never an Origin header, so the app sends none.Every exchange that would look a code up, of either grant, is first admitted in the instance’s budgets of unauthenticated lookups, those of API keys and device tokens: one attempt at a time per client address, 30 failed attempts a minute per address and 300 per IPv6 /48, and at most half the auth pool for all guesses of the instance. Otherwise it answers
429 auth.rate_limitedwithRetry-Afterat once, whatever the code, without using it; retry afterRetry-After. A code that exchanges gives its attempt back. Two phones that scan one QR code at once are not refused for racing: their attempts run one after the other, so the second still revokes the session of the first. -
Meanwhile the browser polls
GET /api/v1/device_pairings/{id}every two seconds or so. The answer has no code;statusispendinguntil a device uses the code or it expires, thenpaired, withdevice_session(id,name,created_at), while that session exists,refusedonce a device used the code without getting a session (a refused exchange, or a second use that revoked it), orexpired. -
When the browser stops showing a code nobody used, it withdraws it with
DELETE /api/v1/device_pairings/{id}andIf-Match: <etag>; the code then answersinvalid_grant. Theetagidentifies the issuance, not the status: it stays the same when the status changes.
Only the browser session that issued a pairing reads or withdraws it. “Sign out other sessions” and a password change that signs out the others delete pending pairing codes, like sign-in codes. The paired device appears in the device list and is revoked like any other.
What each pairing operation can answer, besides 500 server.internal and 503 server.unavailable:
| Operation | Answers |
|---|---|
POST /device_pairings |
201; 400 request.invalid for a malformed Content-Length; 401 auth.unauthenticated without a session cookie, or with a device token or an API key, even next to one; 403 auth.forbidden without an active organization, for a person no longer in it, or with another Authorization credential next to the cookie; 403 request.untrusted_origin without the instance’s Origin; 413 request.too_large and 415 request.unsupported_media_type for a body too large or not JSON (a JSON body is ignored) |
GET /device_pairings/{id} |
200; 400 request.invalid for a malformed identifier (id.invalid) or Content-Length; 401 auth.unauthenticated; 404 device_pairing.not_found for another browser session’s or person’s pairing, a withdrawn one, or one removed an hour after its expiry; 415 for a body without a JSON Content-Type |
DELETE /device_pairings/{id} |
204; 400 request.invalid without If-Match, or for a malformed one, identifier or Content-Length; 401; 403 request.untrusted_origin; 404 device_pairing.not_found; 409 device_pairing.used once a device used the code, paired or refused (sign that device out instead); 412 device_pairing.version_mismatch for an If-Match other than the pairing’s etag; 413; 415 |
POST /device_sessions with a pairing code |
201; 400 device_session.invalid_grant or device_session.invalid_request as above; 400 request.invalid for a body that is neither grant or for a malformed Content-Length; 400 request.malformed for a body that is not valid JSON; 413; 415; 429 auth.rate_limited, with Retry-After, when the exchange is not admitted (the sign-in grant too) |
The three pairing operations never answer 429: a device token never reaches a lookup on them.
Use the token
Section titled “Use the token”Send Authorization: Bearer gcd_… to every /api/v1 operation that lists the deviceSession
security scheme, with GuestCraft-Workspace-Id where the operation needs a workspace. The token
acts with the live role, permissions and property scope of the person in the organization that
was active in the browser: a role change or a removal applies on the next request. A request with a
device token is never authenticated by a cookie, and needs no Origin header.
GET /api/v1/workspaces lists the workspaces the person reaches, with role, permissions and
property_ids (null for all properties), for a workspace and property picker.
Each instance admits device-token requests by the budgets of API keys (API revision 3.9.1, see
API keys). A token it remembers gets 600 requests a minute. Any other
token, such as on its first request to an instance after the instance starts, is a guess, looked
up once: its other requests sent meanwhile wait for that lookup and get its answer, so the app’s
parallel first reads all succeed. Guesses of other tokens from the same client address are looked
up one at a time; they, and requests the budgets refuse, get 429 auth.rate_limited at once with
Retry-After, including GET and DELETE /api/v1/device_sessions, so retry after Retry-After.
Unknown tokens from one address, or one IPv6 /48, use up its failure budget, never a remembered
token’s.
Device tokens cannot issue codes, manage the team, administer Channex, or call the identity routes
under /api/auth. Channex administration (the connection and its credential, the account mode,
upstream property, room-type and rate-plan discovery, links, mappings, webhooks, import drains and
retries, ARI publication changes, and the Messages installation and synchronization) is
browser-only since API 3.7.0. Every browser-only operation (the team, the issuance of codes and
Channex administration) answers 401 auth.unauthenticated to a request that carries a device
token, even next to a valid session cookie (ADR 0041). Channex state and health reads,
conversations, replies and templates accept the token.
List and revoke
Section titled “List and revoke”| Operation | Credentials | Result |
|---|---|---|
GET /api/v1/device_sessions |
browser session or device token | the person’s live sessions, most recently used first; current marks the caller’s own |
DELETE /api/v1/device_sessions/{id} |
browser session (with Origin) or device token, If-Match: <etag> |
204; the token answers 401 from its next request |
DELETE answers 404 device_session.not_found for someone else’s or an ended session, and 412 device_session.version_mismatch when the etag is another issuance’s. The app signs out by
revoking its own session. Account settings in the web list the same sessions with a sign-out per
device.
A session also ends 30 days after its last use, 180 days after its creation, when the person signs
out their other sessions, and when they change their password with “sign out other sessions”. The
last two also void the person’s codes that no device has exchanged yet. A code is exchanged only
while the browser session that allowed it exists: once that browser signs out, or is signed out
by another, the exchange answers 400 device_session.invalid_grant.
Creation, revocation and a reused code are audited as device_session.created,
device_session.revoked and device_session.code_reused in each workspace whose team lists the
person, without the device name, code or token.
Not available yet
Section titled “Not available yet”Push notifications, native passkeys, refresh tokens, and a device session for more than one organization at a time.