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, DELETE) 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:projects | Project endpoints |
mcp:checks | Check endpoints and /v1/regions |
mcp:jobs | Job endpoints, and creating jobs from a slug ping |
mcp:signals | Sending slug pings |
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.
- Project, check, and job creates, updates, pauses, and resumes require member.
- Status-page announcement writes and project, check, and job deletes 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.
Service keys
A service key belongs to the workspace instead of a person. Use one for CI pipelines, Terraform, and other automation that must keep working when people join or leave the team.
| Personal key | Service key | |
|---|---|---|
| Owned by | The user who created it | The workspace |
| Role | The owner's current role, capped at admin | Fixed when the key is created: member or admin |
| Expiry | Optional | Required, at most 365 days ahead |
| Who can create it | The user, for themselves | Member-role keys: owners and admins. Admin-role keys: owners only. |
| When the creator leaves | Stops working | Keeps working |
Service keys use the same ionh_ format, permission levels, and resource allowlist as personal keys. They are available on plans that include service keys.
A service key that cannot be used returns 401 SERVICE_KEY_INACTIVE on every request. This covers a disabled or expired key, a deleted or unsubscribed workspace, and a plan without service keys. The response never says which reason applies; check the key's state in the workspace. A service key whose fixed role is below an endpoint's minimum gets 403 INSUFFICIENT_ROLE, like any other key.
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 has its own rate limits, separate from the dashboard API and from MCP. Every limit is a one-minute window:
| Limit | Applies to | Size |
|---|---|---|
| Per key | Each API key, or each OAuth app installation | Set by the workspace plan |
| Per workspace | All keys in the workspace combined | Twice the per-key limit |
| Per IP address | Every request, before authentication | A high backstop against abuse |
| Failed authentications per IP address | Requests rejected with 401 | 100 per minute |
When a limit is exceeded, the API returns 429 with a Retry-After header in seconds. The body names the limit that was hit in scope, which is principal, workspace, or ip:
{
"statusCode": 429,
"error": "Too Many Requests",
"code": "RATE_LIMIT_EXCEEDED_KEY",
"message": "Per-key rate limit exceeded (60 requests per minute on the FREE plan).",
"limit": 60,
"scope": "principal",
"plan": "FREE"
}plan is left out for OAuth apps and for IP limits. Wait for Retry-After before you retry. Requests sent while blocked are rejected and still count against the per-IP limit.
Successful responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset for your key's budget. Pace bulk work by these headers instead of retrying on 429.
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. |
| 401 | SERVICE_KEY_INACTIVE | A service key cannot be used. The reason is never returned. |
| 401 | INVALID_PRINCIPAL | The credential does not resolve to a valid user. |
| 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 | OAUTH_SCOPE_DENIED | An OAuth app token was not granted the endpoint's resource at the required read or write level. |
| 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). |
| 403 | PLAN_LIMIT_EXCEEDED | A plan limit was hit (response carries detail). See Checks & Jobs. |
| 429 | RATE_LIMIT_EXCEEDED_KEY | A rate limit was exceeded (response carries limit, scope, usually plan, and a Retry-After header). |
| 400 | VALIDATION_FAILED | Request body or query parameters failed validation. |
| 422 | VALIDATION_FAILED | A well-formed value was rejected, such as an unserved probe region. |
| 409 | SLUG_TAKEN, PROJECT_NOT_EMPTY, IDEMPOTENCY_KEY_IN_PROGRESS | Resource conflicts. See Checks & Jobs. |
| 422 | CHECK_TYPE_NOT_SUPPORTED, IDEMPOTENCY_KEY_REUSED | See Checks & Jobs. |
| 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.