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
- In the Ionhour dashboard, open Settings → API Keys.
- Click Create API Key and give it a descriptive name.
- Choose a permission level (read-only or read/write) and, optionally, restrict it to specific resources (see below).
- 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.
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/whoamiThe 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:
| Level | Value | Access |
|---|---|---|
| Read-only | read_only | GET endpoints only. Any write (POST, PATCH) returns 403 KEY_READ_ONLY. |
| Read/write | read_write | All 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 identifier | Covers |
|---|---|
mcp:incidents | Incident endpoints |
mcp:status_pages | Status-page and announcement endpoints |
mcp:deployments | Deployment endpoints |
mcp:maintenance_windows | Maintenance-window endpoints |
mcp:workspace | Workspace introspection (whoami is exempt from the allowlist check) |
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
| HTTP | code | Meaning |
|---|---|---|
| 401 | MISSING_BEARER | No Authorization: Bearer header. |
| 401 | INVALID_KEY | Key is malformed, unknown, or disabled. |
| 401 | KEY_EXPIRED | Key is past its expiry date (response also carries expiredAt). Mint a new key. |
| 403 | INTEGRATION_API_DISABLED | The Integration API is not enabled for this workspace's plan. |
| 403 | KEY_READ_ONLY | A read-only key attempted a write. |
| 403 | KEY_SCOPE_DENIED | The endpoint's resource is not in the key's allowlist (response carries allowedResources). |
| 403 | KEY_OWNER_NO_ACCESS | The key's owner is no longer an active member of the workspace. |
| 403 | INSUFFICIENT_ROLE | The owner's projected role is below the endpoint's minimum (response carries requiredRole and effectiveRole). |
| 403 | DEPENDENCY_PROVIDER_MANAGED | An incident was declared against an Ionhour-managed dependency (see Incidents). |
| 429 | RATE_LIMIT_EXCEEDED_KEY | Per-key rate limit exceeded (response carries limit, plan, and a Retry-After header). |
| 400 | VALIDATION_FAILED | Request body or query parameters failed validation. |
| 404 | NOT_FOUND | Resource not found, or it belongs to another workspace. |
| 500 | INTERNAL_ERROR | Unexpected server error. The detail is logged server-side, never returned. |