Authentication
Create an API key, authenticate against /api/v1, and understand permission levels, resource scoping, role projection, rate limits, and workspace isolation.
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 your avatar menu and choose API Keys — this lands on the Developer Console's Credentials page. Pick the target workspace with the switcher in the page header.
- Click Create token 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. |
Integrations
Build on Ionhour from your own product — declare incidents, post status updates, and receive Ionhour events into your IMS, plus base URL and API conventions.
Incidents
Declare, acknowledge, resolve, and list incidents from your own system through the Ionhour Integration API — the state machine and IMS sync patterns to follow.