Authentication and scopes
Every request carries an API key in the Authorization header:
Authorization: Bearer mtro_your_api_key
There is no OAuth flow, no session exchange, and no refresh step for integrations. The key is the credential.
What a key is bound to
A key is created by a person, inside one organization. It carries both:
- the user who created it — the key can never do more than that person can;
- the entity (organization) that was active at creation — every request is scoped to that organization's properties, bookings, guests, and conversations.
That means a key cannot reach another organization's data, and revoking or downgrading the creating user immediately narrows what the key can do.
Scopes narrow, they never widen
A key may be created with an explicit list of scopes. The rule is worth stating plainly, because it is the opposite of what most APIs do:
- With scopes: the key can perform an action only if that action is in its scope list and the creating user's role allows it.
- Without scopes: the key inherits the creating user's full role permissions.
So a scopeless key is the most powerful kind of key, not the least. Always ask for the narrowest scope list your integration needs.
Administrator accounts are the exception: they cannot create a key without explicit scopes and an explicit expiry.
Expiry and revocation
A key may carry an expiresAt timestamp in milliseconds. On the first request
after that moment the key is deactivated and stops working. Keys can also be
revoked at any time from the admin panel or with POST /api-keys/revoke. Last
use is recorded on every call, so an unused key is easy to spot.
The scopes
These are the 33 actions the platform recognises. The second column counts how many documented endpoints check that scope explicitly — a zero means no endpoint in this documented surface gates on it today, not that the scope is unused elsewhere in the platform.
| Scope | Endpoints that check it |
|---|---|
api_keys.manage | 4 |
applications.read | 1 |
applications.write | 0 |
billing.manage | 0 |
bookings.read | 4 |
bookings.write | 12 |
conversations.read | 6 |
conversations.write | 7 |
creator.manage | 0 |
entity_settings.manage | 0 |
guests.read | 1 |
guests.write | 0 |
inventory.read | 1 |
inventory.write | 3 |
leads.read | 3 |
leads.write | 8 |
leases.sign | 2 |
payments.read | 4 |
payments.write | 11 |
properties.create | 1 |
properties.read | 2 |
properties.write | 3 |
reports.read | 2 |
stripe_accounts.manage | 0 |
tasks.read | 5 |
tasks.write | 3 |
team.manage | 0 |
templates.read | 2 |
templates.write | 8 |
twilio_numbers.manage | 0 |
users.read | 0 |
vendors.read | 3 |
vendors.write | 3 |
Endpoints without a listed scope
Many endpoints do not gate on a named scope: they require a valid key for the entity and then filter results by what the creating user can see. The API reference states the required scope for each endpoint, or says that any valid key for the entity is enough.
An endpoint with no listed scope is not an open endpoint. It still needs a valid key, and it still only returns that organization's data.
Failure modes
| Situation | Status | Body |
|---|---|---|
| No header, malformed header, unknown key | 401 | {"message":"Unauthorized.","error":true} |
| Revoked or expired key | 401 | {"message":"Unauthorized.","error":true} |
| Valid key, missing scope | 403 | {"error":true,"message":"..."} |
| Valid key, resource in another organization | 403 | {"error":true,"message":"..."} |
Keeping the key safe
The key grants direct access to guest personal data, lease documents, and payment records. Treat it like a password:
- store it in a secret manager, never in source control or a frontend bundle;
- use a separate key per integration, so one can be revoked without breaking the others;
- set an expiry and rotate;
- never send it from a browser — every call should go through your own backend.