Ionhour Docs
Integrations

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/v1

A 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/whoami

Two 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 identifierCoversReadCreate, update, pause, resumeDelete
mcp:projects/v1/projectsviewermemberadmin
mcp:checks/v1/checks, /v1/regionsviewermemberadmin
mcp:jobs/v1/jobsviewermemberadmin

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.

MethodPathPurpose
GET/v1/projectsList projects. Filter name is an exact match.
GET/v1/projects/:idGet one project.
POST/v1/projectsCreate. name is required, description optional.
PATCH/v1/projects/:idPartial update. description: null clears it.
DELETE/v1/projects/:idDelete 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.

MethodPathPurpose
GET/v1/checksList. Filters: projectId, checkType, status, slug, page, limit.
GET/v1/checks/:idGet one check. :id is a numeric id or slug:<slug>.
POST/v1/checksCreate. Returns 201.
PATCH/v1/checks/:idPartial update.
POST/v1/checks/:id/pausePause probing. Idempotent; returns the check.
POST/v1/checks/:id/resumeResume probing. Idempotent; returns the check.
DELETE/v1/checks/:idSoft delete. Returns 204.
GET/v1/regionsProbe 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

FieldTypeDefaultNotes
namestringrequired1 to 255 characters.
projectIdintegerrequiredMust be a project in the key's workspace. PATCH moves the check to another project.
slugstring or nullnullSee Slugs.
checkTypestringHTTPOnly HTTP today. See Check types.
intervalSecondsinteger30010 to 3600. Your plan may set a higher floor.
graceSecondsinteger55 to 60.
muteNotificationsbooleanfalseSetting true is plan-gated.
downSeverityP1 to P4P1Severity of DOWN incidents.
lateSeverityP1 to P4P3Severity of LATE incidents.
pausedbooleanread-onlyChange it with /pause and /resume.
statusstringread-onlyNEW, 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:

FieldDefaultNotes
urlrequiredhttp or https, up to 2048 characters. Immutable after create.
methodGETGET, HEAD, POST, PUT, or PATCH.
customHeadersnullUp to 10 headers, values up to 2048 characters. Names are lowercased on write and read back lowercased. Plan-gated.
timeoutMs80001000 to 15000.
expectedStatusMin200100 to 599. Must not exceed expectedStatusMax.
expectedStatusMax399100 to 599.
regionsenvironment default1 to 6 unique region ids from GET /v1/regions. When omitted, the response shows the default regions the scheduler uses.
followRedirectstrue
maxRedirects30 to 3.
blockHttpDowngradetrueBlock HTTPS to HTTP redirects.
failAfterConsecutive31 to 10 consecutive failures before DOWN.
resolveAfterConsecutiveSuccess21 to 10 consecutive successes before recovery.
latencyWarnMsnull1 or more. Plan-gated.
sslExpiryWarnDays141 to 365. Never null; sending null resets it to 14. Setting a value is plan-gated.
bodyContainsnullUp to 1000 characters. Plan-gated.
bodyNotContainsnullUp to 1000 characters. Plan-gated.
ipFamilyAUTORead-only.
skipTlsVerificationfalseRead-only.
maxResponseBytes262144Read-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"
}
FieldDefaultNotes
namerequired1 to 255 characters.
projectIdrequiredPATCH moves the job to another project in the same workspace. The id and ping token stay the same.
slugnullSee Slugs.
intervalSeconds360010 to 604800.
graceSeconds3005 to 86400.
muteNotifications, downSeverity, lateSeverityas for checks
pingToken, pingUrlread-onlyNever 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 /resume return the whole resource, the same shape GET returns.
  • Partial patch. PATCH changes only the keys you send. Inside config, keys merge one by one.
  • Explicit null. On create, null is the same as leaving the key out. On PATCH, slug: null clears the slug; null on any other top-level key is a 400. Inside config, null clears a nullable field and resets any other field to its default. config.url cannot be nulled.
  • Empty patch. A PATCH with 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, pass slug:<slug> instead, for example GET /v1/jobs/slug:nightly-backup.
  • Filter by slug. GET /v1/checks?slug=public-api returns 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:

HTTPcodeWhen
400VALIDATION_FAILEDMalformed body: wrong type, out-of-range value, or unknown key.
422VALIDATION_FAILEDWell-formed but unacceptable value: a reserved header, an unserved region, expectedStatusMin above expectedStatusMax, or a changed config.url.
422CHECK_TYPE_NOT_SUPPORTEDThe check type is not available through the API yet.
403PLAN_LIMIT_EXCEEDEDA plan limit was hit: check, job, or outbound counts, region count, the interval floor, or a plan-gated field.
409SLUG_TAKENThe slug is already used in this workspace.
409PROJECT_NOT_EMPTYThe project still has active checks or jobs.
422IDEMPOTENCY_KEY_REUSEDSame Idempotency-Key, different body.
409IDEMPOTENCY_KEY_IN_PROGRESSThe first request with this key is still in flight.
404NOT_FOUNDNo 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, and ionhour_job resources built on this API.
  • Typed management clients: @ionhour/api for Node.js and ionhour-go/api for Go.

Until then, generate a client from the OpenAPI document or call the API directly.