API Keys
M2M API key authentication for service-to-service integrations
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.
API keys are managed through the Admin API. You need an admin account with the admin:api-keys:manage permission to create keys.
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.
Key Format
All Journey API keys use a jny_ prefix:
jny_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxThe 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
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 |
member auth level is not available for API keys — member authentication always uses Clerk JWT.
/v1/partner/* accepts partner and admin keys only. A service key is
rejected there with 403, even though it works on other surfaces.
Using API Keys
Pass the key as a Bearer token in the Authorization header:
Authorization: Bearer jny_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxThe 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.
X-API-Key: jny_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxconst 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 follow a domain:resource:action format. A key only has access to what its permissions explicitly grant.
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.
// 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
Optionally restrict a key to specific resources:
{
"resourceScope": {
"orgIds": ["org_01J5XYZ"],
"brandIds": ["brand_abc"],
"propertyIds": ["prop_123"],
"global": true // no restriction
}
}Creating a Key
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 onceSee Admin API Keys for the full management API.
Rate Limiting
Each key has a rateLimit (requests per minute, default 1000). Exceeding it returns 429:
{ "statusCode": 429, "message": "Rate limit exceeded" }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
expiresAtat creation time for automatic expiry
Security Best Practices
Never expose API keys in client-side code, browser requests, or public repositories.
// ✅ 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
| Status | Description |
|---|---|
| 401 | Invalid, expired, or revoked key |
| 403 | Key lacks required permission |
| 429 | Key rate limit exceeded |
Bruno Examples
bruno/Admin/API Keys/Create API Key.brubruno/Admin/API Keys/Create Service API Key.brubruno/Admin/API Keys/List API Keys.brubruno/Admin/API Keys/Revoke API Key.brubruno/Admin/API Keys/Query Usage Logs.bru