Creating Monitors by API
POST /projects/{projectId}/monitors creates a Monitor of any type. It takes what the dashboard’s New Monitor form takes, under the same rules, and answers 201 with the Monitor as it was made, its id and its Checks included.
The examples read your token from UUP_API_TOKEN, as Your first API call sets it up. Put your own Project’s id in the path.
Website: http
Section titled “Website: http”A Website Monitor asks a URL for an answer. Name it, give it the url and an intervalSec, and name its locations by their id:
curl -X POST "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors" \ -H "Authorization: Bearer $UUP_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "http", "name": "Website", "url": "https://www.example.com", "intervalSec": 60, "locations": [ "de", "fi", "us" ] }'const response = await fetch("https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors", { method: "POST", headers: { Authorization: `Bearer ${process.env.UUP_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ type: "http", name: "Website", url: "https://www.example.com", intervalSec: 60, locations: [ "de", "fi", "us" ] }),});console.log(await response.json());import jsonimport osimport urllib.request
request = urllib.request.Request( "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors", method="POST", headers={ "Authorization": f"Bearer {os.environ['UUP_API_TOKEN']}", "Content-Type": "application/json", }, data=json.dumps({ "type": "http", "name": "Website", "url": "https://www.example.com", "intervalSec": 60, "locations": [ "de", "fi", "us", ], }).encode(),)with urllib.request.urlopen(request) as response: print(json.load(response))Left out, the request is a GET that expects a 2xx answer, and every Check it can carry is on. request sets the method, headers, body and the statuses that count as up; checks turns Performance, Certificate or Domain off. The reference lists every field.
Port: tcp
Section titled “Port: tcp”A Port Monitor checks that a port on a host takes connections. It needs a host and a port:
curl -X POST "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors" \ -H "Authorization: Bearer $UUP_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "tcp", "name": "Database", "host": "db.example.com", "port": 5432, "intervalSec": 60 }'const response = await fetch("https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors", { method: "POST", headers: { Authorization: `Bearer ${process.env.UUP_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ type: "tcp", name: "Database", host: "db.example.com", port: 5432, intervalSec: 60 }),});console.log(await response.json());import jsonimport osimport urllib.request
request = urllib.request.Request( "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors", method="POST", headers={ "Authorization": f"Bearer {os.environ['UUP_API_TOKEN']}", "Content-Type": "application/json", }, data=json.dumps({ "type": "tcp", "name": "Database", "host": "db.example.com", "port": 5432, "intervalSec": 60, }).encode(),)with urllib.request.urlopen(request) as response: print(json.load(response))This one names no locations, so we choose them for you. See Choosing locations.
Heartbeat: heartbeat
Section titled “Heartbeat: heartbeat”A Heartbeat Monitor waits for your job to call it. Give it a cron Schedule, or an expectedIntervalSec, never both. A graceSec is optional:
curl -X POST "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors" \ -H "Authorization: Bearer $UUP_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "heartbeat", "name": "Nightly backup", "cron": "0 3 * * *" }'const response = await fetch("https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors", { method: "POST", headers: { Authorization: `Bearer ${process.env.UUP_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ type: "heartbeat", name: "Nightly backup", cron: "0 3 * * *" }),});console.log(await response.json());import jsonimport osimport urllib.request
request = urllib.request.Request( "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors", method="POST", headers={ "Authorization": f"Bearer {os.environ['UUP_API_TOKEN']}", "Content-Type": "application/json", }, data=json.dumps({ "type": "heartbeat", "name": "Nightly backup", "cron": "0 3 * * *", }).encode(),)with urllib.request.urlopen(request) as response: print(json.load(response))The answer’s heartbeat Check carries the URL your job calls, in its settings.url. Heartbeat examples show the call for each place a job runs. A heartbeat has no locations: your job calls us.
Server: ping, after a Validation run
Section titled “Server: ping, after a Validation run”A ping Monitor needs a Validation run first, as the dashboard runs one before it saves. A host that does not answer ping looks exactly like a host that is down, so the run proves a reply comes back before the Monitor exists.
- Send the settings to
POST /projects/{projectId}/validations. It answers202with the run’sid, and astateofpending. - Ask
GET /projects/{projectId}/validations/{validationId}every few seconds untilstateis no longerpending. - When it is
ok, create the Monitor with the same settings, itsname, and the run’sidasvalidationId.
# 1. Test the settings with a Validation run. It answers 202, with the run's "id".curl -X POST "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/validations" \ -H "Authorization: Bearer $UUP_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "ping", "host": "gateway.example.com", "intervalSec": 60 }'
# 2. Ask every few seconds until its "state" is no longer "pending".curl -H "Authorization: Bearer $UUP_API_TOKEN" \ "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/validations/val_01J9Z3K8X4M2N6P8Q0R2S4T6V8"
# 3. When it is "ok", create the Monitor: the same settings, a name and the run's "id".curl -X POST "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors" \ -H "Authorization: Bearer $UUP_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "ping", "host": "gateway.example.com", "intervalSec": 60, "name": "Gateway", "validationId": "val_01J9Z3K8X4M2N6P8Q0R2S4T6V8" }'const api = "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8";const headers = { Authorization: `Bearer ${process.env.UUP_API_TOKEN}`, "Content-Type": "application/json",};const settings = { type: "ping", host: "gateway.example.com", intervalSec: 60};
// 1. Test the settings with a Validation run.let response = await fetch(api + "/validations", { method: "POST", headers, body: JSON.stringify(settings),});let validation = await response.json();
// 2. Ask every few seconds until it has an answer.while (validation.state === "pending") { await new Promise((done) => setTimeout(done, 2000)); response = await fetch(api + "/validations/" + validation.id, { headers }); validation = await response.json();}if (validation.state !== "ok") throw new Error("The Validation run is " + validation.state);
// 3. Create the Monitor: the same settings, a name and the run's id.response = await fetch(api + "/monitors", { method: "POST", headers, body: JSON.stringify({ ...settings, name: "Gateway", validationId: validation.id }),});console.log(await response.json());import jsonimport osimport timeimport urllib.request
API = "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8"HEADERS = { "Authorization": f"Bearer {os.environ['UUP_API_TOKEN']}", "Content-Type": "application/json",}
def call(method, path, body=None): data = None if body is None else json.dumps(body).encode() request = urllib.request.Request(API + path, method=method, headers=HEADERS, data=data) with urllib.request.urlopen(request) as response: return json.load(response)
settings = { "type": "ping", "host": "gateway.example.com", "intervalSec": 60,}
# 1. Test the settings with a Validation run.validation = call("POST", "/validations", settings)
# 2. Ask every few seconds until it has an answer.while validation["state"] == "pending": time.sleep(2) validation = call("GET", f"/validations/{validation['id']}")if validation["state"] != "ok": raise SystemExit(f"The Validation run is {validation['state']}.")
# 3. Create the Monitor: the same settings, a name and the run's id.print(call("POST", "/monitors", {**settings, "name": "Gateway", "validationId": validation["id"]}))When the run’s state is failed, its errorKind, statusCode and failedAssertion say what the target answered. Fix that, and start a new run. When it is unanswered, start a new one.
A run proves only the settings it tested. A create that sends other settings is refused with validation_mismatch, and one that waits too long with validation_expired: start a new run, and create again.
A Body assertion, after a Validation run
Section titled “A Body assertion, after a Validation run”A Website Monitor with assertions needs a Validation run too, so you see what the body says before the Monitor relies on it. The steps are the ones above, with the Website settings:
# 1. Test the settings with a Validation run. It answers 202, with the run's "id".curl -X POST "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/validations" \ -H "Authorization: Bearer $UUP_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "http", "url": "https://api.example.com/health", "intervalSec": 60, "assertions": [ { "kind": "json", "path": "$.status", "op": "eq", "value": "ok" } ] }'
# 2. Ask every few seconds until its "state" is no longer "pending".curl -H "Authorization: Bearer $UUP_API_TOKEN" \ "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/validations/val_01J9Z3K8X4M2N6P8Q0R2S4T6V8"
# 3. When it is "ok", create the Monitor: the same settings, a name and the run's "id".curl -X POST "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors" \ -H "Authorization: Bearer $UUP_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "http", "url": "https://api.example.com/health", "intervalSec": 60, "assertions": [ { "kind": "json", "path": "$.status", "op": "eq", "value": "ok" } ], "name": "API health", "validationId": "val_01J9Z3K8X4M2N6P8Q0R2S4T6V8" }'const api = "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8";const headers = { Authorization: `Bearer ${process.env.UUP_API_TOKEN}`, "Content-Type": "application/json",};const settings = { type: "http", url: "https://api.example.com/health", intervalSec: 60, assertions: [ { kind: "json", path: "$.status", op: "eq", value: "ok" } ]};
// 1. Test the settings with a Validation run.let response = await fetch(api + "/validations", { method: "POST", headers, body: JSON.stringify(settings),});let validation = await response.json();
// 2. Ask every few seconds until it has an answer.while (validation.state === "pending") { await new Promise((done) => setTimeout(done, 2000)); response = await fetch(api + "/validations/" + validation.id, { headers }); validation = await response.json();}if (validation.state !== "ok") throw new Error("The Validation run is " + validation.state);
// 3. Create the Monitor: the same settings, a name and the run's id.response = await fetch(api + "/monitors", { method: "POST", headers, body: JSON.stringify({ ...settings, name: "API health", validationId: validation.id }),});console.log(await response.json());import jsonimport osimport timeimport urllib.request
API = "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8"HEADERS = { "Authorization": f"Bearer {os.environ['UUP_API_TOKEN']}", "Content-Type": "application/json",}
def call(method, path, body=None): data = None if body is None else json.dumps(body).encode() request = urllib.request.Request(API + path, method=method, headers=HEADERS, data=data) with urllib.request.urlopen(request) as response: return json.load(response)
settings = { "type": "http", "url": "https://api.example.com/health", "intervalSec": 60, "assertions": [ { "kind": "json", "path": "$.status", "op": "eq", "value": "ok", }, ],}
# 1. Test the settings with a Validation run.validation = call("POST", "/validations", settings)
# 2. Ask every few seconds until it has an answer.while validation["state"] == "pending": time.sleep(2) validation = call("GET", f"/validations/{validation['id']}")if validation["state"] != "ok": raise SystemExit(f"The Validation run is {validation['state']}.")
# 3. Create the Monitor: the same settings, a name and the run's id.print(call("POST", "/monitors", {**settings, "name": "API health", "validationId": validation["id"]}))A keyword assertion looks for a word in the body; a json one reads one value with a path, such as $.status, and compares it. Website and API Monitors says what each one checks.
Choosing locations
Section titled “Choosing locations”A Monitor that is not a heartbeat is checked from one or more locations. Say which in one of three ways:
locations: the ones you want, by theirid.GET /locationslists them.locationCount: how many. We choose which.- Neither: we choose them, as many as the Project’s Default locations in its settings.
Name locations or locationCount, never both. A Monitor has as many locations as your Plan allows, and an odd number of them. See Locations.
When we choose, a Monitor’s locations in the answer says which were chosen. Change them later with PATCH, as you would in the Monitor’s settings.
Finding a Monitor
Section titled “Finding a Monitor”GET /projects/{projectId}/monitors lists a Project’s Monitors. Add q to find one by part of its name or target, whatever the case: checkout finds Checkout API and https://shop.example.com/checkout. It works beside type and state.
curl -H "Authorization: Bearer $UUP_API_TOKEN" \ "https://api.uup.dev/v1/projects/prj_01J9Z3K8X4M2N6P8Q0R2S4T6V8/monitors?q=checkout"When the Plan has no room
Section titled “When the Plan has no room”A Monitor more than your Plan allows is refused with 402 and not_entitled. The body says what was refused, on which Plan, its limit and how many are used, and its message says it as the dashboard does:
{ "error": { "code": "not_entitled", "message": "Your Free plan includes 5 Monitors, and all 5 are in use. Delete one, or move to a larger plan.", "kind": "monitor", "plan": "Free", "limit": 5, "used": 5 }}Delete a Monitor, or move to a larger Plan, and send it again. A paused Monitor counts too. More locations than the Plan allows are refused the same way, with kind set to location: name fewer.
Errors and limits lists every other refusal a create can meet.