Skip to content

Open a shared-handset session for a staff member

POST
/v1/handset/session
curl --request POST \
--url https://api.aerandil.com/v1/handset/session \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "employee_id": "example", "passcode": "example" }'

A shared floor handset (a device of kind handset) holds a client credential whose only scope is handset:session. With that bearer, the attendant presents their employee_id and PIN; the kernel verifies the PIN against the Argon2id hash on the staff_member, refuses any role that is not a floor role (housekeeper / housekeeping_supervisor / engineer / runner), and mints a user token whose principal IS that staff_member, carrying the role’s per-action scope vector (no catch-all actions:apply, no guest PII), bound to the staff member’s house (property_id). Lifetime is AERANDIL_STAFF_SESSION_TTL (a shift). Every rejection reads the same and takes the same time — unknown employee, no PIN set, wrong PIN, wrong role, a member of staff at no one house. Rate-limited per IP like /oauth/token.

Media typeapplication/json
object
employee_id
required

The employee’s HR / payroll identifier — staff_member’s business key, what is printed on the badge.

string
passcode
required

The PIN typed on the handset (6–12 digits).

string
Examplegenerated
{
"employee_id": "example",
"passcode": "example"
}

Session opened; the token runs as the staff member.

Media typeapplication/json
object
access_token
required

Short-lived HS256 JWT. Send as Authorization: Bearer <token> on subsequent calls.

string
expires_in
required

Token lifetime in seconds. Default 3600 (one hour); deployment-configurable via AERANDIL_TOKEN_TTL_SECS.

integer
>= 1
scope
required

Space-separated list of granted scopes. The set the token actually carries — already narrowed by any scope request filter and the client’s pre-provisioned grants.

string
token_type
required

Always Bearer in v1.

Allowed value: Bearer
property_id
required

The house the token is bound to — the staff member’s home (staff_works_at_property), else the house their record is scoped to. Every read, verb and function the token reaches is held to it; another house’s answers 403 property_out_of_scope.

string format: uuid
role
required

The floor role the scope vector was derived from.

string
Allowed values: housekeeper housekeeping_supervisor engineer runner
staff_member_id
required

The staff_member the token runs as — also the token’s User principal id, so “who am I” is a plain object read.

string format: uuid
Example
{
"token_type": "Bearer",
"role": "housekeeper"
}
Cache-Control
string

Always no-store. Object and link state is live data and is never cacheable.

The credentials were rejected (no detail by design).

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

The bearer does not hold handset:session.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

Per-IP rate limit exceeded (the /oauth/token limiter).