Skip to content

Authentication & keys

Every request authenticates with an API key in the Authorization header — no cookies, no OAuth dance, no signatures:

Authorization: Bearer hr_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

The full key is displayed once at creation; the platform stores only its SHA-256 hash. Treat it like a password: environment variable or secret manager, never a repository — and never in browser code, where a key would be handed to every visitor.

Verify a new key with GET …/me on its door: it answers with the agency, the door, whom the key is bound to, the key’s scopes and its budgets.

A key belongs to exactly one door (its audience), fixed when it is created. The door decides which base URL the key may call at all; a request to another door answers 403 AUDIENCE_MISMATCH, whatever the scopes say.

DoorCreated inBound toMay call
agencyAgency console → Partner API— (the whole agency)/partner/v1/*, /mcp
landlordLandlord portal → API access, or the console on their behalfone landlord/partner/v1/landlord/*
cleaningCleaning portal → API access, or the console on their behalfone cleaning crew/partner/v1/cleaning/*

GET /partner/v1/me is the one path every door may call — it is how a key finds out which door it is.

The binding is the data scope of every request: a landlord key sees the units inside that landlord’s tenures and nothing else, a cleaning key sees that crew’s jobs and nothing else. There is deliberately no parameter to widen it. The agency console lists every key of every door and can revoke any of them.

Each key carries an explicit set of scopes from its door’s catalogue — a landlord key cannot hold read:guests, because that scope does not exist for its door. Requests beyond the key’s scopes fail with 403 SCOPE_MISSING; create keys with the minimum your integration needs.

Plain read scopes are pre-selected when a key is created. Scopes that write or expose personal data are not: they are ticked deliberately or not at all — and an existing key never gains an ability because a new one was added.

ScopeGrants
read:unitsGET /units, GET /units/:slug, GET /amenities, MCP list_units
read:availabilityGET /availability, MCP get_availability
read:ratesPOST /quote, GET /rates, GET /promotions, GET /fee-types, MCP quote_stay
read:bookingsGET /bookings, GET /bookings/:id, MCP list_bookings
read:guestsGET /guests, GET /guests/:id — personal data; also unlocks the contact block on enquiries and the buyer block on invoices
write:bookingsPOST /bookings, POST /bookings/:id/cancel
read:requestsGET /requests, GET /requests/:id
read:tasksGET /tasks, GET /tasks/:id
write:tasksPOST /tasks, PATCH /tasks/:id
read:financeGET /invoices, GET /invoices/:id, GET /payments, GET /deposits
read:housekeepingGET /housekeeping/jobs, GET /housekeeping/teams
read:landlordsGET /landlords, GET /landlords/:id, GET /landlords/:id/statement
read:costsGET /unit-costs, GET /cost-categories
ScopeGrants
landlord:read:unitsGET /units, GET /units/:slug, GET /availability, GET /bookings, GET /bookings/:id
landlord:read:financeGET /statement, GET /payouts, GET /invoices
landlord:read:costsGET /costs, GET /cost-categories
landlord:write:costsPOST /costs, DELETE /costs/:id — writes
landlord:write:blocksPOST /blocks, DELETE /blocks/:id — writes
landlord:read:documentsGET /documents, GET /documents/:id/download
landlord:messagesGET /messages, GET /messages/:id, POST /messages, POST /messages/:id/replies — writes
ScopeGrants
cleaning:read:jobsGET /units, GET /jobs, GET /plan
cleaning:write:reportsPOST /jobs/:key/start, POST /jobs/:key/finish, POST /jobs/:key/reopen — writes

Writes through any door are recorded in the agency’s audit trail with the key as the actor, including refused attempts. The MCP endpoint is agency-only and stays read-only.

  • Rotate by creating a second key, deploying it, then revoking the old one — both stay valid in between, so rotation needs no downtime.
  • Revoke any key instantly in the portal that created it, or in the agency console. Revocation is immediate for new requests and permanent.
  • Keys can carry an optional expiry date at creation; expired keys answer 401 like revoked ones.
  • The portals show a last used timestamp per key (minute precision), so a forgotten integration is easy to spot — and a dead key easy to retire.
HTTPcodeMeaning
401UNAUTHORIZEDMissing/malformed header, unknown, revoked or expired key, or the agency account is not active
403FEATURE_DISABLEDThe agency’s plan does not include API access
403AUDIENCE_MISMATCHThe key belongs to another door; the response names the door and its base path
403SCOPE_MISSINGKey is valid but lacks the scope; the response names it
429RATE_LIMITEDOver 60 requests in the rolling minute for this key — respect Retry-After
429QUOTA_EXCEEDEDThe agency’s 50,000 requests for this month are spent (budgets)
{ "code": "AUDIENCE_MISMATCH", "title": "This is a landlord key — it can only call /partner/v1/landlord/*", "audience": "landlord", "basePath": "/partner/v1/landlord" }