Get started

Authentication

Integrations hold a long-lived API key and trade it for a 15-minute access token. This page covers issuing keys, scoping them to fewer permissions, and revoking them.

There are two credentials, and it is worth keeping them straight.

AspectAPI keyAccess token
Looks likemagic_1830472847398404096_Xk3nQv…a JWT, eyJ…
LivesUntil revoked, or until it expires15 minutes
HeldStored by your integration, like a passwordIn memory, then discarded
Used forProving who you are, once per sessionEvery actual API call

Your integration holds the key, exchanges it for a token, sends the token on every request, and exchanges again when the token runs out.

This is not OAuth

There is no refresh token, and no grant_type, client_id, or client_secret. The API key is the durable credential and it is the only thing you store.

That is deliberate. A refresh token would be a second rotating secret to keep, and MagicDoor's refresh tokens rotate with reuse detection — so two concurrent refreshes would lock the caller out.

The endpoint takes no authentication — the key in the body is the credential. It lives on the authentication host, not on the service you are calling.

curl --request POST \
  --url 'https://auth.magicdoor.com/api-keys/token' \
  --header 'Content-Type: application/json' \
  --data '{"key": "magic_1830472847398404096_Xk3nQv8mWp2sT5yR9dL4bN6hJ1kF3gZ7xC0vQ8aE"}'
200 OK
{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "tokenType": "Bearer",
  "expiresIn": 900
}

Then send the token on every request:

GET /company-app/properties HTTP/1.1
Host: api.portal.magicdoor.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

expiresIn is in seconds. Re-exchange a little early rather than waiting for a 401 and retrying.

Every failure is the same 401

A malformed key, an unknown key, a revoked key, an expired key, a wrong secret, a deactivated user, a user who moved to another company, a suspended company — all return 401 with an identical message. The real reason goes to MagicDoor's logs and never to the response, so that a caller probing keys learns nothing.

When a key stops working, check it in MagicDoor: is it still listed, and is it revoked or expired? If it looks healthy and still fails, contact support.

magic_1830472847398404096_Xk3nQv8mWp2sT5yR9dL4bN6hJ1kF3gZ7xC0vQ8aE
└─┬─┘ └────────┬────────┘ └──────────────────┬───────────────────┘
prefix      key id                         secret
  • The secret half is shown exactly once, in the response that creates the key. MagicDoor stores only a hash, so a lost key must be replaced — it can never be recovered or re-sent.
  • Treat the whole string as the secret. Never split it, and never put it in a URL query string. Body or header only.
  • The magic_ prefix is deliberate, so secret scanners can recognise a MagicDoor key that has leaked into a public repository.

Most people never call these endpoints. To create a key by hand, open the profile menu in the top-right corner of MagicDoor and choose Developer — or go straight to portal.magicdoor.com/settings/account/developer — then click Create API key, give it a name and an expiration date, and click Create key. Copy the key before you close the dialog; it is not shown again.

The endpoints below are the same thing over HTTP, for building your own key management. They require a signed-in property manager session, so an integration cannot use them to mint itself more keys — it only ever exchanges the key it was given.

MethodEndpointRequires
POST/company-portal/api-keysapi_keys:write
GET/company-portal/api-keysapi_keys:read
GET/company-portal/api-keys/{apiKeyId}api_keys:read
DELETE/company-portal/api-keys/{apiKeyId}api_keys:delete, and it must be your own key

All four are on the authentication service:

https://auth.magicdoor.com
POST /company-portal/api-keys
{
  "name": "Nightly rent-roll sync",
  "permissions": ["properties:read", "units:read", "leases:read"],
  "expires": "2027-01-01T00:00:00"
}
200 OK — the only time key is ever returned
{
  "apiKey": {
    "id": "1830472847398404096",
    "name": "Nightly rent-roll sync",
    "userId": "8812004417765",
    "permissions": ["properties:read", "units:read", "leases:read"],
    "created": "2026-08-17T21:04:11",
    "expires": "2027-01-01T00:00:00"
  },
  "key": "magic_1830472847398404096_Xk3nQv8mWp2sT5yR9dL4bN6hJ1kF3gZ7xC0vQ8aE"
}
Request body
namestringrequired

How the key is labelled in MagicDoor, 1–100 characters. Purely for telling keys apart later — make it say what uses it.

example: Nightly rent-roll sync
permissionsstring[]

What the key may do, as resource:action strings. Must be a subset of your own permissions. Leave this out to issue an unscoped key that can do whatever you can.

example: ["properties:read","leases:read"]
expiresstring · date-time

When the key stops working, in your company's timezone. Leave it out for a key that never expires on its own.

example: 2027-01-01T00:00:00
The key is in this response and nowhere else

key appears once, here. There is no endpoint that returns it again. Store it before you discard the response.

A key always acts as the person who created it, and can never do more than that person can.

Leave permissions out and the key is unscoped: it does whatever its user can do, re-read on every exchange — so it also picks up any permission that user is granted later. That is the broader choice.

Give an explicit list and the key is frozen at that set and can only ever narrow from there. This is the one to recommend: a key that reads the rent roll has no business writing leases.

Narrowing a person narrows their keys

Reduce a property manager's own permissions and every key they created is reduced with them, immediately. No key has to be found or edited — each one is measured against what its owner can do at the moment it is exchanged.

Narrowing intersects rather than discards. A key granted leases:* whose owner is later limited to leases:read keeps read access rather than losing leases altogether. If nothing survives the intersection, the exchange returns 401 like any other failure.

GET /company-portal/permissions on the authentication service returns every permission a property manager can hold, for building a picker. It takes optional category and search query parameters, and only ever returns company portal permissions — tenant, owner, vendor and MagicDoor-internal ones are not reachable from a property manager's session.

[
  {
    "code": "leases:read",
    "name": "Read Leases",
    "description": "View lease details, list leases, search leases.",
    "category": "Leases",
    "portal": "CompanyPortal"
  }
]

Asking for a permission you do not hold yourself returns 403, naming the ones at fault: “You cannot grant permissions you do not have: leases:write”.

Listing returns every key in the company, so an administrator can see who has issued what. It pages with page and pageSize (1–100, default 50), and filters on userId and revoked (true, false, or omitted for both).

Revoking returns the revoked key with revoked set, so a UI can show when it stopped working. Revoking twice is harmless and does not move the revocation time. There is no un-revoke.

You can see a colleague's keys but not revoke them

Revoking someone else's key returns 401, even with api_keys:delete. To kill a departing employee's keys, deactivate the person — that stops every key they issued at once, because each exchange re-checks the owner.

  • A key always acts as the person who created it. You cannot issue one on someone else's behalf.
  • A key belongs to one company. If its owner moves to another company, the key stops working rather than following them.
  • Only a key's name can be changed. Permissions and expiry are fixed at creation — changing scope means issuing a new key and retiring the old one.
  • Keys cannot be created while MagicDoor support is shadowing your account. Otherwise a credential would outlive the support session and be indistinguishable from one you made yourself.
  • Requests made with a key are marked in MagicDoor's logs, so support can tell a script from a person.