Exchange a portal session or sign-in code for a scoped user token
const url = 'https://api.aerandil.com/v1/portal/token';const options = { method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{"code":"example","scope":"example","session":"example"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.aerandil.com/v1/portal/token \ --header 'Content-Type: application/json' \ --data '{ "code": "example", "scope": "example", "session": "example" }'Exchange a human’s verified portal session (WorkOS) — or the authorization code from the AuthKit redirect, which the kernel exchanges with WorkOS itself — for a tenant-scoped bearer token that runs as a User principal (cloud 0006 §10).
The human behind the token is a staff_member when one claims the identity (Ithaca’s desk, 2026-09-09): a staff record whose identity_subject is the provider’s subject — or, on a code exchange, the one active employee carrying the provider’s verified email, bound on that first sign-in through update_staff_member_profile. The token’s principal is then that record, the scope vector follows the employee’s role (a tenant admin keeps the admin grant on top), and the lifetime is a shift (AERANDIL_STAFF_SESSION_TTL). An inactive staff record that claims the identity is refused (403). Otherwise the token runs as the provider subject with the organization role’s vector for the service-token lifetime. An optional scope parameter narrows within the grant but never widens it.
Public endpoint — the caller authenticates with the portal session, not a prior bearer. Returns 501 when no identity provider is configured on the cell.
Request Bodyrequired
Section titled “Request Bodyrequired”Exactly one of session or code: an access token the caller already holds, or the authorization code from the identity provider’s redirect.
object
The authorization code from the identity provider’s redirect (WorkOS AuthKit, response_type=code). The kernel exchanges it with the provider using its own API key — a browser surface never holds a provider token — and verifies the resulting session exactly as session.
Optional space-separated scope filter. The issued token carries the intersection of this and the human’s role-derived scope vector — a portal can request less than the role grants, never more.
An access token the caller already holds, validated by the cell’s configured identity provider (WorkOS).
Examplegenerated
{ "code": "example", "scope": "example", "session": "example"}Responses
Section titled “Responses”Token minted. scope is the role-derived set, intersected with the scope request parameter when present; staff_member_id names the employee the token runs as, when one claims the identity.
object
Short-lived HS256 JWT. Send as Authorization: Bearer <token> on subsequent calls.
Token lifetime in seconds. Default 3600 (one hour); deployment-configurable via AERANDIL_TOKEN_TTL_SECS.
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.
Always Bearer in v1.
The User principal the token runs as: the staff_member’s own id when one claims the signed-in identity, else the stable uuid derived from the identity provider’s subject.
The role the scope vector was derived from: the employee’s (front_desk, front_office_manager, housekeeper, housekeeping_supervisor, engineer, runner) when a staff record claims the identity, else the organization role (viewer, member, admin).
The staff_member the human resolved to — by identity_subject, or on a code exchange by the one active employee carrying the provider’s verified email (bound on the spot through update_staff_member_profile). null for an operator with no staff record.
Example
{ "token_type": "Bearer"}The portal session or code was missing, expired, or rejected.
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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 staff record that claims this identity is inactive.
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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" }}More than one staff record claims this identity (ambiguous_staff_identity).
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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" }}Neither or both of session and code were given.
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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" }}No identity provider is configured on this cell.
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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" }}