# Create an API key

Source: https://api-docs.magicdoor.com/api-reference/api-keys/create-api-key

Issues a new API key that acts as the calling user, and returns the secret in the response. This is the only time the secret is ever sent — MagicDoor stores a hash of it and nothing else. Requires the `api_keys:write` permission.

`POST https://auth.magicdoor.com/company-portal/api-keys`

**Authentication:** bearer token in the `Authorization` header, as `Bearer <token>`.

## Body

Content type: `application/json`

- `name` (string, required) — How the key is labelled in MagicDoor, 1 to 100 characters. It is only used for telling keys apart later, so make it say what uses the key. Example: `Nightly rent-roll sync`.
- `permissions` (string[]) — What the key may do, as `resource:action` strings, and at least one entry when present. Every entry must be something the calling user already holds. Leave the whole field out to issue an unscoped key that does whatever the calling user can do, re-read on every exchange, including permissions granted later. Example: `["properties:read","units:read","leases:read"]`.
- `expires` (string, date-time) — When the key should stop working, given in the company's timezone and with no offset. Leave it out for a key that does not expire on its own. Example: `2027-01-01T00:00:00`.

```json
{
  "name": "Nightly rent-roll sync",
  "permissions": [
    "properties:read",
    "units:read",
    "leases:read"
  ],
  "expires": "2027-01-01T00:00:00"
}
```

## Example request

```bash
curl --request POST \
  --url 'https://auth.magicdoor.com/company-portal/api-keys' \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Nightly rent-roll sync",
  "permissions": [
    "properties:read",
    "units:read",
    "leases:read"
  ],
  "expires": "2027-01-01T00:00:00"
}'
```

## Responses

### 200 — The new key, secret included

- `apiKey` (object) — The stored record for the new key.
  - `id` (string) — This key's id, a Snowflake sent as a decimal string. It is also the middle segment of `key`.
  - `name` (string) — The label that was sent in the request.
  - `userId` (string) — The user the key acts as, which is always the caller. A key cannot be issued on someone else's behalf.
  - `permissions` (string[]) — The permissions that were requested. Absent when no `permissions` were sent, which means the key is unscoped.
  - `created` (string, date-time) — When the key was issued, in the company's timezone and with no offset.
  - `expires` (string, date-time) — When the key stops working, in the company's timezone. Absent for a key that does not expire on its own.
  - `lastUsed` (string, date-time) — When the key was last exchanged for an access token. Always absent on a key that has just been created.
  - `revoked` (string, date-time) — When the key was revoked. Always absent on a key that has just been created.
- `key` (string) — The credential the integration stores and sends to the token endpoint. It appears here and in no other response, ever.

```json
{
  "apiKey": {
    "id": "1830472847398404096",
    "name": "Nightly rent-roll sync",
    "userId": "1824903117441335296",
    "permissions": [
      "properties:read",
      "units:read",
      "leases:read"
    ],
    "created": "2026-08-17T21:04:11",
    "expires": "2027-01-01T00:00:00"
  },
  "key": "magic_1830472847398404096_Xk3nQv8mWp2sT5yR9dL4bN6hJ1kF3gZ7xC0vQ8aE"
}
```

### 400 — The body failed validation — for example a name outside 1 to 100 characters, or an empty `permissions` array

### 401 — The access token is missing, expired, or invalid

### 403 — The caller does not hold `api_keys:write`, or asked for a permission they do not hold themselves, or is being shadowed by MagicDoor support

```json
{
  "type": "https://httpstatuses.com/403",
  "title": "Forbidden",
  "status": 403,
  "detail": "You cannot grant permissions you do not have: leases:write",
  "instance": "/company-portal/api-keys",
  "traceId": "0HN7GK9C2M4PQ"
}
```

> **Danger: The secret is in this response and nowhere else**
>
> `key` is returned once, here. No endpoint returns it again, and MagicDoor cannot re-send it because only a hash is stored. Store it before you discard the response, or issue a replacement key.

## What the key is allowed to do

A key always acts as the person who created it and can never do more than that person can. Sending an explicit `permissions` list freezes the key at that set; leaving it out gives an unscoped key that follows its user, widening as well as narrowing.

Either way the set is narrowed again to the user's current permissions on every exchange, so reducing a person reduces every key they issued. Only the name of an existing key can be changed — changing scope or expiry means issuing a new key and revoking the old one. Use [List permissions](https://api-docs.magicdoor.com/api-reference/permissions/list-permissions.md) to see the values `permissions` accepts.

> **Warning: Not while support is shadowing you**
>
> If a MagicDoor support agent is acting as you, this call returns `403` with the detail *API keys cannot be created while shadowing a user.* Otherwise the session would leave behind a credential that outlives it and looks exactly like one you made yourself.
