# API Keys
> Source: /api-reference/authentication/api-keys
> M2M API key authentication for service-to-service integrations

# API Key Authentication [#api-key-authentication]

Journey API keys provide secure machine-to-machine (M2M) access for backend services, integrations, and automated workflows. The key system uses **direct permissions** and optional **resource scopes** — not role-based access.

<Note>
  API keys are managed through the Admin API. You need an admin account with the `admin:api-keys:manage` permission to create keys.
</Note>

<Note>
  These keys do not authenticate HXP Threads, messaging event polling, or Action
  commands. HXP's operator MCP also requires its own audience-bound OAuth token,
  not a `jny_` key. See [HXP integration availability](/hxp/overview).
</Note>

## Key Format [#key-format]

All Journey API keys use a `jny_` prefix:

```
jny_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

The first 8 characters (the prefix) are stored publicly and appear in logs and admin views. The full value is shown **only once** at creation time and cannot be retrieved again.

## Auth Levels [#auth-levels]

API keys carry an `authLevel` that controls which surfaces they can access:

| Auth Level | Access | Use Case |
|------------|--------|----------|
| `partner` | Partner-scoped endpoints | Hotel partners, external integrations |
| `admin` | Admin API endpoints | Internal tooling, dashboards |
| `service` | Cross-surface service access | PMS integrations, webhook delivery, automation |

<Note>
  `member` auth level is not available for API keys — member authentication always uses Clerk JWT.
</Note>

<Warning>
  `/v1/partner/*` accepts `partner` and `admin` keys only. A `service` key is
  rejected there with `403`, even though it works on other surfaces.
</Warning>

## Using API Keys [#using-api-keys]

Pass the key as a Bearer token in the `Authorization` header:

```http
Authorization: Bearer jny_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

<Warning>
  **The Partner API is the exception.** Routes under `/v1/partner/*` read the
  key from `X-API-Key`; on those routes `Authorization: Bearer` is parsed as a
  WorkOS access token, so a `jny_` key sent there returns `401`. Accepting the
  key as a Bearer token everywhere is the target behaviour — see
  [Partner authentication](/api-reference/partner/authentication).

  ```http
  X-API-Key: jny_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  ```
</Warning>

```typescript
const response = await fetch('https://api.journey.com/public/reservations/lookup', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.JOURNEY_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ ... })
});
```

## Permissions [#permissions]

Permissions follow a `domain:resource:action` format. A key only has access to what its permissions explicitly grant.

### Common Permissions [#common-permissions]

| Permission | Description |
|------------|-------------|
| `reservations:read` | Read reservation data |
| `reservations:write` | Create and modify reservations |
| `members:read` | Read member profiles |
| `admin:api-keys:read` | List and view API keys |
| `admin:api-keys:manage` | Full API key management |

You can assign **permission groups** when creating a key — these expand to a flat list of permissions at creation time. Call `GET /admin/api-keys/permission-groups` to see available groups.

```typescript
// Using permission groups
{ "permissionGroups": ["reservations-read-write"] }

// Using explicit permissions
{ "permissions": ["reservations:read", "reservations:write"] }

// Both merged and deduplicated
{ "permissions": ["members:read"], "permissionGroups": ["reservations-read-write"] }
```

## Resource Scopes [#resource-scopes]

Optionally restrict a key to specific resources:

```typescript
{
  "resourceScope": {
    "orgIds": ["org_01J5XYZ"],
    "brandIds": ["brand_abc"],
    "propertyIds": ["prop_123"],
    "global": true   // no restriction
  }
}
```

## Creating a Key [#creating-a-key]

```typescript
const response = await fetch('https://api.journey.com/admin/api-keys', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${workosToken}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'PMS Cloudbeds Integration',
    serviceId: 'pms-cloudbeds',
    authLevel: 'service',
    permissionGroups: ['reservations-read-write'],
    rateLimit: 500,
    expiresAt: '2025-12-31T23:59:59Z'
  })
});

const { apiKey, ...keyRecord } = await response.json();
// apiKey: "jny_xxxx..." — store securely, shown only once
```

See Admin API Keys for the full management API.

## Rate Limiting [#rate-limiting]

Each key has a `rateLimit` (requests per minute, default 1000). Exceeding it returns `429`:

```json
{ "statusCode": 429, "message": "Rate limit exceeded" }
```

## Key Lifecycle [#key-lifecycle]

- **Regenerate**: `POST /admin/api-keys/:id/regenerate` — new secret, old one immediately invalidated
- **Revoke**: `POST /admin/api-keys/:id/revoke` — permanent, cannot be undone
- **Expire**: set `expiresAt` at creation time for automatic expiry

## Security Best Practices [#security-best-practices]

<Warning>
  Never expose API keys in client-side code, browser requests, or public repositories.
</Warning>

```typescript
// ✅ Good: Environment variables
const apiKey = process.env.JOURNEY_API_KEY;

// ❌ Bad: Hardcoded in source
const apiKey = 'jny_live_abc123...';
```

Grant only the permissions your service needs. Rotate keys on a schedule or immediately when potentially compromised.

## Error Responses [#error-responses]

| Status | Description |
|--------|-------------|
| 401 | Invalid, expired, or revoked key |
| 403 | Key lacks required permission |
| 429 | Key rate limit exceeded |

## Bruno Examples [#bruno-examples]

- `bruno/Admin/API Keys/Create API Key.bru`
- `bruno/Admin/API Keys/Create Service API Key.bru`
- `bruno/Admin/API Keys/List API Keys.bru`
- `bruno/Admin/API Keys/Revoke API Key.bru`
- `bruno/Admin/API Keys/Query Usage Logs.bru`
