Monitoring you can drive from a shell script
Everything a dashboard can do for a monitor is a curl away, and the rest is a URL you ping.
Create it, verify it, read it back
The request, the CI step that uses it, and the JSON that comes back.
curl
curl -X POST https://app.tenium.app/api/v1/monitors \
-H "Authorization: Bearer $TENIUM_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"api-prod","type":"http",
"url":"https://api.example.com/health",
"intervalSec":60,"expectedStatus":["2xx"]}' GitHub Action
name: deploy
on: { push: { branches: [main] } }
jobs:
deploy:
runs-on: ubuntu-latest
env:
B: https://app.tenium.app/api/v1
ID: ${{ vars.TENIUM_MONITOR_ID }}
H: "Authorization: Bearer ${{ secrets.TENIUM_KEY }}"
steps:
- uses: actions/checkout@v4
- run: curl -fsS -X POST -H "$H" "$B/monitors/$ID/pause"
- run: ./deploy.sh
- if: always()
run: curl -fsS -X POST -H "$H" "$B/monitors/$ID/resume"
- name: Verify from outside
run: |
curl -fsS -X POST -H "$H" "$B/monitors/$ID/check"
sleep 20
curl -fsS -H "$H" "$B/monitors/$ID" | jq -e '.status == "up"' Response
{
"id": "a3f1c2d4-9e8b-4f6a-b7c5-1d2e3f4a5b6c",
"name": "api-prod",
"type": "http",
"url": "https://api.example.com/health",
"intervalSec": 60,
"expectedStatus": ["2xx"],
"status": "pending",
"paused": false,
"nextCheckAt": "2026-01-20T10:15:03Z",
"createdAt": "2026-01-20T10:15:00Z"
} Write keys need Pro or Agency; read-only keys work on every plan.
Illustrative mock-up
Three things that matter here
A plain REST API
JSON in and out, bearer keys, cursors. Read keys on every plan, write keys on Pro and Agency.
Signed webhooks
incident.opened, acknowledged and resolved events with an HMAC-SHA256 signature from Pro. Failed deliveries are retried.
Assertions on the body
A JSON path with equals, contains or greater than, from Starter. A 200 that says the queue is stuck still counts as down.
Alerts go where this role already works
E-mail, Discord and Google Chat on every plan; Slack, Telegram, Teams and Mattermost from Starter; webhooks from Pro; Zapier and PagerDuty on Agency. No SMS and no voice calls.
- Slack
- Telegram
- Discord
- PagerDuty
- Zapier
- Webhook
A service, defined in the repository
Illustrative scenario, not a customer story
- The infrastructure repo adds a monitor definition next to the service.
- The pipeline posts it to the API with a write key stored as a CI secret.
- After the rollout a step calls the check endpoint and fails the job if the service is not up from outside.
The monitor exists because the code exists. Nobody has to remember to add it.
What does an hour of downtime cost you?
A rough, honest number to weigh against the price of a monitor. It runs in your browser and nothing is sent anywhere.
For comparison: Starter costs $7 a month. Revenue is not the whole cost (support time, ads still running, trust), so treat this as a floor.
Create the monitor with the code that ships the service
A monitor that someone has to remember to add by hand is a monitor that will be missing the day it matters. If your infrastructure already lives in a repository, put the check there too. A write-scoped API key, a JSON body and one request is enough:
curl -X POST https://app.tenium.app/api/v1/monitors \
-H "Authorization: Bearer $TENIUM_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"api-prod","type":"http","url":"https://api.example.com/health",
"intervalSec":60,"expectedStatus":["2xx"]}' The response returns the new monitor with its id, status and next check time. Write access belongs to Pro ($29) and Agency ($59), while read access, including a key you can hand to a dashboard or a Slack bot, is on all plans including Free. The endpoint list is on the REST API page, and every field is documented under API: monitors.
Assertions that check what the response says
A health endpoint that returns 200 while its database is gone is the classic lying check. From Starter ($7) upward you can assert on the body instead of trusting the status line. For a JSON endpoint use a path expression: $.status equals "ok", $.queue.depth less than 100, or $.checks.db exists. For HTML responses use a keyword rule that must contain or must not contain a string or regex. Multiple rules combine with AND, so an endpoint is down if any rule fails.
Custom request headers arrive on Pro, which is what you need to send a bearer token or a shared secret that lets the monitor through a WAF. Header values are masked in every response. Read the API monitoring and keyword monitoring pages for the options.
Heartbeats are one line at the end of a script
For anything that runs on a schedule, flip the direction. Your job requests a URL when it finishes, and the monitor opens an incident if that request is late. There is nothing to install:
# crontab
0 3 * * * /usr/local/bin/backup.sh \
&& curl -fsS -m 10 https://ping.tenium.app/h/abc123 >/dev/null \
|| curl -fsS -m 10 https://ping.tenium.app/h/abc123/fail Add /start at the top of the script to measure run time. GitHub Actions, GitLab CI, systemd timers and Kubernetes CronJobs all fit the same pattern, because all of them can run curl. See cron job monitoring and the heartbeat reference.
Webhooks and the event stream
On Pro a webhook contact receives JSON for incidents opening, being acknowledged and resolving, and for SSL and domain expiry, response-time alerts and late heartbeats. Each delivery is signed. A minimal verifier in Node is a dozen lines using createHmac and timingSafeEqual, and the docs include it. Treat the receiver like any public endpoint: verify, check the timestamp, answer 2xx quickly and do slow work afterwards. Non-2xx answers retry five times over about half an hour. Payload shapes: webhook payloads.
Badges and public status
For open-source projects and internal portals, an uptime badge is a small SVG you can embed in a README. Generate one with the uptime badge tool and paste the markup. If you want a human-readable page, the status pages feature publishes component history on your own domain from Starter upward.
Limits you should know before you build on it
- Reads: 600 requests per minute per key. Writes: 120 per minute per key. Responses carry
X-RateLimit-*headers. - Pagination is cursor-based, with a default of 50 results and a maximum of 200.
- Each monitor is a single request; multi-step flows and browser scripting are not supported.
- The API is versioned in the path (
/v1); additive changes are not versioned, breaking ones get six months of overlap.
Prefer to ask an assistant to do this for you? See the honest status of our MCP server plans.
Developer questions
Which plan do I need for API writes?
Every plan can create a read-only key and list monitors, incidents and checks. Creating, changing and deleting things requires a write-scoped key on Pro ($29) or Agency ($59). Without it you get a 403 with the code plan_limit.
Do heartbeats need an SDK?
No. A heartbeat is a URL you request with curl, wget or any HTTP client, using GET or POST. Suffixes mark start, finish and failure, and you can post up to 10 KB of text with the request.
Can I assert on a JSON response?
Yes, from Starter upward. Use dot and bracket paths such as $.data.items[0].id with equals, not equals, contains, greater than, less than or exists. A 200 whose body says the queue is stuck still counts as down.
How are webhook calls authenticated?
Each request carries an X-Tenium-Signature header with a timestamp and an HMAC-SHA256 of the body using the contact's secret. Verify it and reject old timestamps. Failed deliveries are retried with backoff.
Is there an MCP server or SDK?
No SDK today, and the MCP server is planned rather than built. The REST API is the supported route, and the plain curl examples in the docs are intentionally all you need.
Create a read-only key and look around
Free plan, no card. Settings, then API keys, then New key.