Ionhour Docs
Integrations

Authentication

Create an API key, authenticate against /api/v1, and understand permission levels, resource scoping, rate limits, and error codes.

The Integration API authenticates with API keys — the same ionh_ keys used by the MCP integration. Every key is bound to a single workspace, and every request is scoped to that workspace.

Create an API key

  1. In the Ionhour dashboard, open Settings → API Keys.
  2. Click Create API Key and give it a descriptive name.
  3. Choose a permission level (read-only or read/write) and, optionally, restrict it to specific resources (see below).
  4. Copy the key immediately — the full value is shown only once.

API keys have the format ionh_ followed by 32 hex characters. Ionhour stores only a SHA-256 hash plus a short display prefix; the plaintext is never retrievable after creation.

Treat API keys like passwords. Store them in your secret manager and never commit them to source control.

Use the key

Pass the key as a Bearer token on every /api/v1 request:

curl -H "Authorization: Bearer ionh_your_api_key_here" \
  https://api.ionhour.com/api/v1/whoami

The whoami endpoint is the recommended smoke test — it validates the key and echoes back the resolved context without touching any resource:

{
  "workspaceId": 42,
  "workspaceName": "Acme Production",
  "permissionLevel": "read_write",
  "projectedRole": "admin",
  "apiKeyPrefix": "ionh_1a2b"
}
whoami is resource-agnostic: it works even for a key that has been restricted to a narrow set of resources, so you can always confirm a key authenticates.

Permission levels

Each key carries one of two permission levels:

LevelValueAccess
Read-onlyread_onlyGET endpoints only. Any write (POST, PATCH) returns 403 KEY_READ_ONLY.
Read/writeread_writeAll endpoints the key's role and resources allow.

Resource scoping

A key can optionally be restricted to an allowlist of resources. When set, the key may only call endpoints that map to one of the allowed resources; anything else returns 403 KEY_SCOPE_DENIED. Leaving the allowlist empty means the key can reach every resource.

The Integration API and MCP share the same resource identifiers, so a restriction you set in the dashboard applies identically over both surfaces.

Resource identifierCovers
mcp:incidentsIncident endpoints
mcp:status_pagesStatus-page and announcement endpoints
mcp:deploymentsDeployment endpoints
mcp:maintenance_windowsMaintenance-window endpoints
mcp:workspaceWorkspace introspection (whoami is exempt from the allowlist check)
The identifiers use underscores (mcp:status_pages), not hyphens. These are the exact strings the guard checks.

Role projection

A key never has more authority than its owner. The guard projects the owner's current workspace role through the key, capped at ADMIN, and enforces a minimum role per endpoint:

  • Reads (GET) require viewer.
  • Incident, deployment, and maintenance writes require member.
  • Status-page announcement writes require admin.

If the owner is no longer an active member of the workspace, the key is rejected with 403 KEY_OWNER_NO_ACCESS. If the owner's role is below what an endpoint requires, the response is 403 INSUFFICIENT_ROLE.

Workspace scoping

Every query and mutation is scoped to the key's workspace. A resource id that belongs to another workspace is treated as if it does not exist — you get 404 NOT_FOUND, never a cross-tenant leak.

Rate limits

The Integration API applies a per-key request-rate limit, separate from the global per-IP limit and from the MCP surface's budget. The limit is derived from the workspace plan. When exceeded, the API returns 429 with a Retry-After header and this body:

{
  "statusCode": 429,
  "error": "Too Many Requests",
  "code": "RATE_LIMIT_EXCEEDED_KEY",
  "message": "Per-key rate limit exceeded (30 requests per minute on the FREE plan).",
  "limit": 30,
  "plan": "FREE"
}

Back off until the window resets (honor Retry-After), then retry.

Error format

Every /api/v1 error uses a stable envelope. Pattern-match on code, not on the human-readable message:

{
  "statusCode": 403,
  "error": "Forbidden",
  "code": "KEY_SCOPE_DENIED",
  "message": "This API key is not permitted to access \"mcp:incidents\"."
}

Error codes

HTTPcodeMeaning
401MISSING_BEARERNo Authorization: Bearer header.
401INVALID_KEYKey is malformed, unknown, or disabled.
401KEY_EXPIREDKey is past its expiry date (response also carries expiredAt). Mint a new key.
403INTEGRATION_API_DISABLEDThe Integration API is not enabled for this workspace's plan.
403KEY_READ_ONLYA read-only key attempted a write.
403KEY_SCOPE_DENIEDThe endpoint's resource is not in the key's allowlist (response carries allowedResources).
403KEY_OWNER_NO_ACCESSThe key's owner is no longer an active member of the workspace.
403INSUFFICIENT_ROLEThe owner's projected role is below the endpoint's minimum (response carries requiredRole and effectiveRole).
403DEPENDENCY_PROVIDER_MANAGEDAn incident was declared against an Ionhour-managed dependency (see Incidents).
429RATE_LIMIT_EXCEEDED_KEYPer-key rate limit exceeded (response carries limit, plan, and a Retry-After header).
400VALIDATION_FAILEDRequest body or query parameters failed validation.
404NOT_FOUNDResource not found, or it belongs to another workspace.
500INTERNAL_ERRORUnexpected server error. The detail is logged server-side, never returned.