Ionhour Docs
Integrations

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.

A slug ping reports a job heartbeat by the job's slug instead of its ping token. You authenticate with an API key, so one key can drive many jobs, and a job can be created automatically on its first ping.

Use slug pings when your jobs are defined in code and you would rather not copy a ping token per job. If you already have a job's ping URL, the plain token ping on signal.ionhour.com keeps working and needs no key.

Send a slug ping

Slug pings live on the API host. Send the key as a Bearer token:

curl -X POST https://api.ionhour.com/api/signals/s/nightly-backup \
  -H "Authorization: Bearer ionh_your_api_key"
{ "message": "OK" }
MethodPathSignal
GET or POST/api/signals/s/:slugSuccess
GET or POST/api/signals/s/:slug/failFailure
GET or POST/api/signals/s/:slug/startRun started

The slug must match ^[a-z0-9_-]{1,64}$. A POST may carry a JSON payload of up to 10 KB, stored with the signal like a token ping's payload. These optional query parameters add run telemetry:

ParameterMeaning
ridRun id, up to 64 characters.
hostHost name, up to 253 characters.
envEnvironment label, up to 32 characters.
exit_codeProcess exit code, 0 to 255.
durationRun duration in milliseconds.
curl -X POST "https://api.ionhour.com/api/signals/s/nightly-backup/fail?exit_code=2&duration=48120" \
  -H "Authorization: Bearer ionh_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "error": "disk full" }'

signal.ionhour.com serves only token pings, /api/signals/ping/:token and its /fail and /start variants. Slug pings sent to the signal host fail. Always use api.ionhour.com for slug pings.

Create the job on first ping

Add ?create=1 to create the job when no job in the workspace has that slug yet:

curl -X POST "https://api.ionhour.com/api/signals/s/nightly-backup?create=1" \
  -H "Authorization: Bearer ionh_your_api_key"

When the job is created, the response is 201:

{ "message": "OK", "created": true }
  • The new job is named after its slug and placed in a project called Auto-provisioned. Ionhour creates that project if your plan has room for another project; otherwise the job goes into the workspace's oldest project.
  • The job starts with a one-hour interval and a five-minute grace period. Adjust its interval and grace period afterwards in the dashboard or with PATCH /v1/jobs/slug:<slug>.
  • If the slug already exists, create=1 is ignored and the request is a normal ping that returns 200. SDKs can therefore send create=1 on every ping.
  • Without create=1, an unknown slug returns 404 NOT_FOUND.

Auto-creating jobs is controlled by a server-side feature flag. When it is off for your workspace, create=1 on an unknown slug returns 403 FEATURE_DISABLED. Pings to jobs that already exist are not affected. To control job settings from the start, create the job with the Integration API and ping it by slug.

Key requirements

Every slug ping changes job state, so the key must be able to write:

  • The key must be read/write. A read-only key gets 403 READ_ONLY_KEY.
  • The key must project to at least the member role.
  • If the key is restricted to specific resources, it needs mcp:signals to ping. Creating a job with create=1 also needs mcp:jobs.
  • Personal keys, service keys, and OAuth app tokens are accepted. An OAuth token needs write access to the same resources.

Errors use the Integration API envelope, { statusCode, error, code, message }:

HTTPcodeMeaning
400VALIDATION_FAILEDThe slug does not match the allowed pattern.
401MISSING_BEARERNo Authorization: Bearer header.
401INVALID_KEYThe key is unknown, malformed, or disabled.
401KEY_EXPIREDThe key has expired.
401SERVICE_KEY_INACTIVEThe service key cannot be used.
403READ_ONLY_KEYThe key is read-only.
403KEY_SCOPE_DENIEDThe key's resource list does not include mcp:signals, or mcp:jobs for a create.
403OAUTH_SCOPE_DENIEDThe OAuth app was not granted write access to the resource.
403KEY_OWNER_NO_ACCESSThe key's owner is no longer an active member of the workspace.
403INSUFFICIENT_ROLEThe key projects below the member role.
403FEATURE_DISABLEDcreate=1 was sent, but auto-creating jobs is off for the workspace.
403PLAN_LIMIT_EXCEEDEDCreating the job would exceed the plan's job limit.
404NOT_FOUNDNo job has this slug and create=1 was not sent.

Slug pings are rate-limited per key and slug, so many jobs behind one shared IP address do not starve each other. A generous per-IP limit applies on top.

Deprecated: key in the URL

The earlier form put the API key in the path:

https://api.ionhour.com/api/signals/p/:pingKey/:slug
https://api.ionhour.com/api/signals/p/:pingKey/:slug/fail
https://api.ionhour.com/api/signals/p/:pingKey/:slug/start

This form is deprecated. URLs end up in shell history, CI logs, and proxy logs, which leaks the key. Every response from these routes carries deprecation headers:

Deprecation: @1791676800
Sunset: Sun, 11 Apr 2027 00:00:00 GMT
Link: <https://docs.ionhour.com/api-reference/signals>; rel="deprecation", <https://docs.ionhour.com/api-reference/signals>; rel="sunset"

The route keeps the same behavior and errors as the header form until the sunset date, with one difference: service keys are never accepted in the URL and return 403 KEY_KIND_NOT_ALLOWED.

To migrate, move the key from the path into the Authorization header and change /p/<key>/ to /s/:

# Before (deprecated)
curl "https://api.ionhour.com/api/signals/p/ionh_your_api_key/nightly-backup?create=1"

# After
curl "https://api.ionhour.com/api/signals/s/nightly-backup?create=1" \
  -H "Authorization: Bearer ionh_your_api_key"

If a key has ever appeared in a URL, rotate it after you migrate.