Checks & Jobs
Manage projects, HTTP checks, and heartbeat jobs as code with the Integration API: create, read, patch, pause, and delete with slugs, idempotency, and errors.
The Integration API exposes projects, checks, and jobs as full read/write resources under /api/v1. Use it from CI pipelines, infrastructure-as-code tooling, and scripts to keep your monitoring in step with your services.
The surface is built as a state contract: every field you write comes back from GET in the same shape, defaults are deterministic, ids never change, and a deleted resource returns 404 from then on. That makes it safe to diff, reconcile, and re-apply.
All routes live on the API host:
https://api.ionhour.com/api/v1A machine-readable OpenAPI document for every /api/v1 route is served publicly, with no authentication, at
https://api.ionhour.com/api/v1/openapi.json. Point code generators and API clients at it.
Authentication
Send an API key as a Bearer token on every request, exactly as described in Authentication:
curl -H "Authorization: Bearer ionh_your_api_key" \
https://api.ionhour.com/api/v1/whoamiTwo kinds of key work here:
- Personal keys act as the user who created them. The key's authority follows that user's current workspace role, so a key stops working when its owner leaves the workspace.
- Service keys belong to the workspace, not to a person. They are the right choice for CI and Terraform because they survive team changes. A service key has a fixed role (member or admin) and a mandatory expiry of at most 365 days. Only a workspace owner can create an admin-role service key; owners and admins can create member-role keys.
See Service keys for the full rules.
Resources and roles
Restrict a key to the resources it needs. These are the identifiers and minimum roles for this page:
| Resource identifier | Covers | Read | Create, update, pause, resume | Delete |
|---|---|---|---|---|
mcp:projects | /v1/projects | viewer | member | admin |
mcp:checks | /v1/checks, /v1/regions | viewer | member | admin |
mcp:jobs | /v1/jobs | viewer | member | admin |
A read-only key can call every GET route. Any write needs a read/write key.
Projects
Every check and job belongs to a project.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/projects | List projects. Filter name is an exact match. |
GET | /v1/projects/:id | Get one project. |
POST | /v1/projects | Create. name is required, description optional. |
PATCH | /v1/projects/:id | Partial update. description: null clears it. |
DELETE | /v1/projects/:id | Delete an empty project. Returns 204. |
curl -X POST https://api.ionhour.com/api/v1/projects \
-H "Authorization: Bearer ionh_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-project-payments" \
-d '{ "name": "Payments", "description": "Payment services" }'{
"id": 7,
"name": "Payments",
"description": "Payment services",
"createdAt": "2026-10-11T09:00:00.000Z",
"updatedAt": "2026-10-11T09:00:00.000Z"
}Deleting a project through the API never cascades. If any active check or job remains in the project, the request
fails with 409 PROJECT_NOT_EMPTY. Delete or move the monitors first. This protects monitors created in the dashboard
from being wiped by an infrastructure-as-code replacement.
Checks
Checks are outbound probes that Ionhour runs against a URL you own.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/checks | List. Filters: projectId, checkType, status, slug, page, limit. |
GET | /v1/checks/:id | Get one check. :id is a numeric id or slug:<slug>. |
POST | /v1/checks | Create. Returns 201. |
PATCH | /v1/checks/:id | Partial update. |
POST | /v1/checks/:id/pause | Pause probing. Idempotent; returns the check. |
POST | /v1/checks/:id/resume | Resume probing. Idempotent; returns the check. |
DELETE | /v1/checks/:id | Soft delete. Returns 204. |
GET | /v1/regions | Probe regions you can use in config.regions. |
Create a check
name, projectId, and config.url are required. Everything else falls back to a documented default and is echoed back in the response.
curl -X POST https://api.ionhour.com/api/v1/checks \
-H "Authorization: Bearer ionh_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0b6f1c1e-4e0a-4b51-9a0c-2f0d1d8a7c11" \
-d '{
"name": "Uptime - Public API",
"slug": "public-api",
"projectId": 7,
"intervalSeconds": 300,
"config": {
"url": "https://api.example.com/health",
"method": "GET",
"expectedStatusMin": 200,
"expectedStatusMax": 299,
"regions": ["us-east-1", "eu-west-1"]
}
}'The response is the full check, including every computed default:
{
"id": 123,
"slug": "public-api",
"name": "Uptime - Public API",
"projectId": 7,
"checkType": "HTTP",
"intervalSeconds": 300,
"graceSeconds": 5,
"muteNotifications": false,
"downSeverity": "P1",
"lateSeverity": "P3",
"paused": false,
"status": "NEW",
"config": {
"url": "https://api.example.com/health",
"method": "GET",
"customHeaders": null,
"timeoutMs": 8000,
"expectedStatusMin": 200,
"expectedStatusMax": 299,
"regions": ["us-east-1", "eu-west-1"],
"followRedirects": true,
"maxRedirects": 3,
"blockHttpDowngrade": true,
"failAfterConsecutive": 3,
"resolveAfterConsecutiveSuccess": 2,
"latencyWarnMs": null,
"sslExpiryWarnDays": 14,
"bodyContains": null,
"bodyNotContains": null,
"ipFamily": "AUTO",
"skipTlsVerification": false,
"maxResponseBytes": 262144
},
"createdAt": "2026-10-11T09:05:00.000Z",
"updatedAt": "2026-10-11T09:05:00.000Z"
}Check fields
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | required | 1 to 255 characters. |
projectId | integer | required | Must be a project in the key's workspace. PATCH moves the check to another project. |
slug | string or null | null | See Slugs. |
checkType | string | HTTP | Only HTTP today. See Check types. |
intervalSeconds | integer | 300 | 10 to 3600. Your plan may set a higher floor. |
graceSeconds | integer | 5 | 5 to 60. |
muteNotifications | boolean | false | Setting true is plan-gated. |
downSeverity | P1 to P4 | P1 | Severity of DOWN incidents. |
lateSeverity | P1 to P4 | P3 | Severity of LATE incidents. |
paused | boolean | read-only | Change it with /pause and /resume. |
status | string | read-only | NEW, OK, LATE, DOWN, PAUSED, or RESUMED until the next probe result after a resume. |
The config object
config holds the probe settings for the check's type. For HTTP checks:
| Field | Default | Notes |
|---|---|---|
url | required | http or https, up to 2048 characters. Immutable after create. |
method | GET | GET, HEAD, POST, PUT, or PATCH. |
customHeaders | null | Up to 10 headers, values up to 2048 characters. Names are lowercased on write and read back lowercased. Plan-gated. |
timeoutMs | 8000 | 1000 to 15000. |
expectedStatusMin | 200 | 100 to 599. Must not exceed expectedStatusMax. |
expectedStatusMax | 399 | 100 to 599. |
regions | environment default | 1 to 6 unique region ids from GET /v1/regions. When omitted, the response shows the default regions the scheduler uses. |
followRedirects | true | |
maxRedirects | 3 | 0 to 3. |
blockHttpDowngrade | true | Block HTTPS to HTTP redirects. |
failAfterConsecutive | 3 | 1 to 10 consecutive failures before DOWN. |
resolveAfterConsecutiveSuccess | 2 | 1 to 10 consecutive successes before recovery. |
latencyWarnMs | null | 1 or more. Plan-gated. |
sslExpiryWarnDays | 14 | 1 to 365. Never null; sending null resets it to 14. Setting a value is plan-gated. |
bodyContains | null | Up to 1000 characters. Plan-gated. |
bodyNotContains | null | Up to 1000 characters. Plan-gated. |
ipFamily | AUTO | Read-only. |
skipTlsVerification | false | Read-only. |
maxResponseBytes | 262144 | Read-only. |
Some header names are rejected with 422: host, content-length, transfer-encoding, connection, cookie, authorization, proxy-authorization, proxy-connection, and the x-forwarded-for, x-forwarded-host, x-forwarded-proto, and x-forwarded-port family. Two names that collide after lowercasing are also rejected.
config.url cannot change. To point a check at a new URL, create a new check and delete the old one. Restating the
stored URL in a PATCH is accepted, so tools that send the full object keep working.
Check types
Only HTTP checks can be created and updated through the API today. Sending another known checkType returns 422 CHECK_TYPE_NOT_SUPPORTED; support for each type arrives as it ships.
Checks of other types that were created in the dashboard still appear in lists and can be read, paused, resumed, and deleted. Their HTTP-only config fields read as null, and a PATCH returns 422 CHECK_TYPE_NOT_SUPPORTED.
List regions
curl -H "Authorization: Bearer ionh_your_api_key" \
https://api.ionhour.com/api/v1/regions[
{ "id": "us-east-1", "displayName": "US East (Virginia)", "flag": "🇺🇸", "enabled": true }
]The list contains only regions this environment serves. A check that names any other region is rejected with 422.
Jobs
Jobs are inbound heartbeats: your cron job, worker, or pipeline pings Ionhour, and Ionhour alerts when the ping is late. The seven routes mirror checks under /v1/jobs, with the same filters, roles, and delete behavior.
curl -X POST https://api.ionhour.com/api/v1/jobs \
-H "Authorization: Bearer ionh_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6d2c9a40-7f8e-4f0b-8c1e-1a2b3c4d5e6f" \
-d '{
"name": "Nightly Backup Heartbeat",
"slug": "nightly-backup",
"projectId": 7,
"intervalSeconds": 86400,
"graceSeconds": 600
}'{
"id": 456,
"slug": "nightly-backup",
"name": "Nightly Backup Heartbeat",
"projectId": 7,
"intervalSeconds": 86400,
"graceSeconds": 600,
"muteNotifications": false,
"downSeverity": "P1",
"lateSeverity": "P3",
"paused": false,
"status": "NEW",
"pingToken": "a3f9…",
"pingUrl": "https://signal.ionhour.com/api/signals/ping/a3f9…",
"createdAt": "2026-10-11T09:10:00.000Z",
"updatedAt": "2026-10-11T09:10:00.000Z"
}| Field | Default | Notes |
|---|---|---|
name | required | 1 to 255 characters. |
projectId | required | PATCH moves the job to another project in the same workspace. The id and ping token stay the same. |
slug | null | See Slugs. |
intervalSeconds | 3600 | 10 to 604800. |
graceSeconds | 300 | 5 to 86400. |
muteNotifications, downSeverity, lateSeverity | as for checks | |
pingToken, pingUrl | read-only | Never rotated by PATCH. See below. |
The ping token lets anyone send heartbeats for the job. pingToken and pingUrl are returned only to read/write keys
that project to member or higher and cover mcp:jobs or mcp:signals. For every other caller both fields are
null. If your tool stores them, for example in Terraform state, keep that storage encrypted.
Jobs attached to a dependency instead of a project are not exposed through /v1/jobs. They do not appear in lists and return 404 by id or slug.
Read back, patch, and null
These rules hold for all three resources:
- Full read-back.
POST,PATCH,/pause, and/resumereturn the whole resource, the same shapeGETreturns. - Partial patch.
PATCHchanges only the keys you send. Insideconfig, keys merge one by one. - Explicit null. On create,
nullis the same as leaving the key out. OnPATCH,slug: nullclears the slug;nullon any other top-level key is a400. Insideconfig,nullclears a nullable field and resets any other field to its default.config.urlcannot be nulled. - Empty patch. A
PATCHwith no keys is a read: nothing is written and nothing is audited. - Plan gates on change only. Plan-gated check fields are checked only when their value changes, so restating your current state after a plan downgrade does not fail.
# Clear the slug and reset the timeout to its default
curl -X PATCH https://api.ionhour.com/api/v1/checks/123 \
-H "Authorization: Bearer ionh_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "slug": null, "config": { "timeoutMs": null } }'Pause, resume, and delete
curl -X POST https://api.ionhour.com/api/v1/checks/slug:public-api/pause \
-H "Authorization: Bearer ionh_your_api_key"
curl -X POST https://api.ionhour.com/api/v1/checks/slug:public-api/resume \
-H "Authorization: Bearer ionh_your_api_key"
curl -X DELETE https://api.ionhour.com/api/v1/checks/123 \
-H "Authorization: Bearer ionh_your_api_key"Pause and resume are idempotent. Delete is a soft delete: later reads return 404, and a second delete also returns 404.
Slugs
A slug is a stable, human-chosen name for a check or job. It must match ^[a-z0-9_-]{1,64}$. Check slugs are unique among the workspace's checks, and job slugs among its jobs.
- Address by slug. Anywhere a route takes
:id, passslug:<slug>instead, for exampleGET /v1/jobs/slug:nightly-backup. - Filter by slug.
GET /v1/checks?slug=public-apireturns at most one item. - Conflicts. Creating or renaming to a slug that is already taken returns
409 SLUG_TAKEN. - Released on delete. Deleting a check or job frees its slug in the same step, so you can reuse it right away. A resource restored later in the dashboard comes back without a slug.
Job slugs are also what slug pings use to find, and optionally create, a job.
Idempotent create
Every POST that creates a resource accepts an Idempotency-Key header. Send a new random value, such as a UUID, for each logical create, and reuse it when you retry that same create.
- The key is 1 to 128 printable ASCII characters with no spaces.
- Keys are scoped to your workspace, your key, the method, and the route, and kept for 24 hours.
- A retry with the same key and the same body returns the original status and a fresh read of the resource, with the header
Idempotency-Replayed: true. - A retry with the same key and a different body returns
422 IDEMPOTENCY_KEY_REUSED. - If the first request is still running, a concurrent retry waits up to 5 seconds, then returns
409 IDEMPOTENCY_KEY_IN_PROGRESS. Retry after a short delay.
Only successful responses and deterministic errors, such as 409 SLUG_TAKEN or a 422 validation error, are remembered. Transient failures are not, so a retry runs again.
Errors
Errors use the standard envelope, { statusCode, error, code, message }. Match on code. The codes specific to these resources:
| HTTP | code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Malformed body: wrong type, out-of-range value, or unknown key. |
| 422 | VALIDATION_FAILED | Well-formed but unacceptable value: a reserved header, an unserved region, expectedStatusMin above expectedStatusMax, or a changed config.url. |
| 422 | CHECK_TYPE_NOT_SUPPORTED | The check type is not available through the API yet. |
| 403 | PLAN_LIMIT_EXCEEDED | A plan limit was hit: check, job, or outbound counts, region count, the interval floor, or a plan-gated field. |
| 409 | SLUG_TAKEN | The slug is already used in this workspace. |
| 409 | PROJECT_NOT_EMPTY | The project still has active checks or jobs. |
| 422 | IDEMPOTENCY_KEY_REUSED | Same Idempotency-Key, different body. |
| 409 | IDEMPOTENCY_KEY_IN_PROGRESS | The first request with this key is still in flight. |
| 404 | NOT_FOUND | No such resource in the key's workspace, including deleted ones. |
PLAN_LIMIT_EXCEEDED carries a detail object. resource names the capped resource, such as checks or jobs, or the plan-gated field, such as regions or bodyContains:
{
"statusCode": 403,
"error": "Forbidden",
"code": "PLAN_LIMIT_EXCEEDED",
"message": "Your PRO plan allows a maximum of 3 probe region(s). You selected 4. Please upgrade your plan to add more.",
"detail": { "limit": 3, "plan": "PRO", "resource": "regions" }
}OAuth apps never receive the plan field. Authentication and permission errors are listed in the error-code table.
Rate limits
Requests are limited per minute at three levels:
- Per key. Each API key, or each OAuth app installation, has its own budget set by the workspace plan.
- Per workspace. All keys in a workspace together get twice the per-key budget.
- Per IP address. A high backstop that only stops abuse.
A request over any limit returns 429 RATE_LIMIT_EXCEEDED_KEY with a Retry-After header in seconds. The body names the scope that was exceeded: principal, workspace, or ip. Wait for Retry-After before retrying. Requests sent while blocked are still rejected and still count against the IP limit.
Successful responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset for your key's budget. When reconciling many resources, pace requests by these headers rather than retrying on 429.
Coming next
- An official Terraform provider with
ionhour_project,ionhour_check, andionhour_jobresources built on this API. - Typed management clients:
@ionhour/apifor Node.js andionhour-go/apifor Go.
Until then, generate a client from the OpenAPI document or call the API directly.
Deployments & Maintenance
Announce deploys and maintenance windows from your own CI so Ionhour pauses probing or suppresses alerts while you work — including a GitHub Actions example.
Slug Pings
Send job heartbeats by slug with an API key in the Authorization header, auto-create jobs on first ping, and migrate off the deprecated key-in-URL slug route.