Skip to content

Migrate from UptimeRobot

Copy your UptimeRobot monitors to OKStatus with one command, run both side by side for a few days, then switch. Your UptimeRobot account is never changed.

A Python script reads your monitors with the UptimeRobot API and recreates them with the OKStatus API. It only uses the standard library: nothing to install.

  • A read-only UptimeRobot key: Integrations → API → Read-only API key. It can’t change anything.
  • A Full OKStatus key, on the Max plan (Settings → API Keys). On Free or Pro, the dry run of step 3 still gives you the exact list to recreate by hand.
  • Python 3.9 or later.
UptimeRobotOKStatusDetail
HTTP(s)HTTPsame URL, same method
KeywordHTTP + keyword“not exists” becomes keywordInvert
PingPing
PortTCPHTTP 80, HTTPS 443, FTP 21, SMTP 25, POP3 110, IMAP 143 or your custom port
HeartbeatHeartbeat (push)a new ping URL for your cron jobs; periods over one day are skipped
PausedPausedthe monitor is created, then paused
IntervalIntervalraised to your plan’s minimum when needed, at most one day

Download uptimerobot_to_okstatus.py, or copy it:

uptimerobot_to_okstatus.py
#!/usr/bin/env python3
"""Copy UptimeRobot monitors to OKStatus.
Reads your monitors with UptimeRobot's API (v2, read-only key is enough) and
creates them in OKStatus with its REST API (Full key, Max plan). Without
--apply it only prints what it would create. Your UptimeRobot account is
never changed. Python 3.9+, standard library only.
export UPTIMEROBOT_API_KEY=ur...
export OKSTATUS_REGIONS=fr-dc3,de-fra,nl-ams # regions of your plan
export OKSTATUS_MIN_INTERVAL=30 # 300 Free, 60 Pro, 30 Max
python3 uptimerobot_to_okstatus.py # dry run
export OKSTATUS_API_KEY=oks_live_...
python3 uptimerobot_to_okstatus.py --apply
Guide: https://okstatus.eu/docs/tutorials/migrate-from-uptimerobot/
"""
import json
import os
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
UR_API = os.environ.get("UPTIMEROBOT_API_URL", "https://api.uptimerobot.com/v2")
OK_API = os.environ.get("OKSTATUS_API_URL", "https://api.okstatus.eu/api/v1")
REGIONS = [r.strip() for r in os.environ.get("OKSTATUS_REGIONS", "fr-dc3,de-fra").split(",") if r.strip()]
MIN_INTERVAL = int(os.environ.get("OKSTATUS_MIN_INTERVAL", "60"))
APPLY = "--apply" in sys.argv
# UptimeRobot v2 codes (uptimerobot.com/api/legacy)
PORTS = {1: 80, 2: 443, 3: 21, 4: 25, 5: 110, 6: 143} # port monitor sub_type
METHODS = {1: "HEAD", 2: "GET", 3: "POST", 4: "PUT", 5: "PATCH", 6: "DELETE", 7: "OPTIONS"}
PAUSED = 0 # monitor status
def uptimerobot_monitors():
key = os.environ.get("UPTIMEROBOT_API_KEY")
if not key:
sys.exit("Set UPTIMEROBOT_API_KEY (Integrations → API → read-only key).")
offset = 0
while True:
form = {"api_key": key, "format": "json", "offset": offset, "limit": 50,
"http_request_details": "true"}
req = urllib.request.Request(f"{UR_API}/getMonitors", urllib.parse.urlencode(form).encode())
with urllib.request.urlopen(req, timeout=30) as r:
page = json.load(r)
if page.get("stat") != "ok":
sys.exit(f"UptimeRobot: {page.get('error')}")
yield from page.get("monitors", [])
offset += 50
if offset >= int(page.get("pagination", {}).get("total", 0)):
return
def as_int(value, default=0):
try:
return int(value)
except (TypeError, ValueError):
return default
def convert(m):
"""(body of POST /api/v1/monitors, None) or (None, why it is skipped)."""
kind = as_int(m.get("type"))
body = {
"name": str(m.get("friendly_name") or m.get("url") or "Monitor")[:100],
"regions": REGIONS,
# OKStatus accepts 30 s to 1 day, and never less than the plan's floor.
"interval": min(max(as_int(m.get("interval"), 300), MIN_INTERVAL), 86400),
}
if kind in (1, 2): # HTTP(s), Keyword
body.update(type="http", url=m["url"])
method = METHODS.get(as_int(m.get("http_method"), 2), "GET")
if method != "GET":
body["httpMethod"] = method
if kind == 2:
body["keyword"] = m.get("keyword_value") or ""
body["keywordInvert"] = as_int(m.get("keyword_type")) == 2 # 2 = "not exists"
elif kind == 3: # Ping
body.update(type="ping", url=m["url"])
elif kind == 4: # Port
sub = as_int(m.get("sub_type"))
port = as_int(m.get("port")) if sub == 99 else PORTS.get(sub)
if not port:
return None, "port monitor without a port"
body.update(type="tcp", url=m["url"], port=port)
elif kind == 5: # Heartbeat
if as_int(m.get("interval")) > 86400:
# A shorter period would report the job down between two runs.
return None, "heartbeat period over one day, the OKStatus maximum"
slug = re.sub(r"[^a-z0-9]+", "-", body["name"].lower()).strip("-")
body.update(type="push", url=slug or "heartbeat")
else:
return None, f"UptimeRobot type {m.get('type')} has no equivalent"
return body, None
def okstatus(method, url, body):
req = urllib.request.Request(url, json.dumps(body).encode(), method=method, headers={
"Authorization": f"Bearer {os.environ['OKSTATUS_API_KEY']}",
"Content-Type": "application/json",
})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["data"]
def main():
if APPLY and not os.environ.get("OKSTATUS_API_KEY"):
sys.exit("Set OKSTATUS_API_KEY (a Full key, Settings → API Keys) to use --apply.")
created = skipped = failed = 0
for m in uptimerobot_monitors():
name = m.get("friendly_name", "?")
body, why = convert(m)
if body is None:
print(f"SKIPPED {name} ({why})")
skipped += 1
continue
paused = as_int(m.get("status"), -1) == PAUSED
if not APPLY:
print(f"TO CREATE {body['type']:5} {body['name']} -> {body['url']}"
+ (" (paused)" if paused else ""))
continue
try:
new = okstatus("POST", f"{OK_API}/monitors", body)
if paused:
okstatus("PATCH", f"{OK_API}/monitors/{new['id']}", {"status": "paused"})
extra = f" · new ping URL: {new['pushUrl']}" if body["type"] == "push" else ""
print(f"CREATED {body['name']}{extra}")
created += 1
except urllib.error.HTTPError as e:
print(f"FAILED {body['name']}: {e.code} {e.read().decode(errors='replace')[:200]}")
failed += 1
if APPLY:
print(f"\n{created} created, {skipped} skipped, {failed} failed.")
if failed:
sys.exit(1)
else:
print("\nDry run: nothing created. Add --apply to create these monitors.")
if __name__ == "__main__":
main()
Terminal window
export UPTIMEROBOT_API_KEY=ur…
export OKSTATUS_REGIONS=fr-dc3,de-fra,nl-ams # regions of your plan
export OKSTATUS_MIN_INTERVAL=30 # 300 on Free, 60 on Pro, 30 on Max
python3 uptimerobot_to_okstatus.py
TO CREATE http Website -> https://example.com
TO CREATE http API health -> https://api.example.com/health
TO CREATE tcp SMTP -> mail.example.com
TO CREATE push Nightly backup -> nightly-backup
TO CREATE ping Office router -> 203.0.113.10 (paused)
Dry run: nothing created. Add --apply to create these monitors.

Nothing is created without --apply. Read the list, adjust the variables, run it again as often as you need.

Terminal window
export OKSTATUS_API_KEY=oks_live_…
python3 uptimerobot_to_okstatus.py --apply
CREATED Website
CREATED API health
CREATED SMTP
CREATED Nightly backup · new ping URL: https://backend.okstatus.eu/push/…
CREATED Office router
5 created, 0 skipped, 0 failed.
  1. Recreate your alert channels in the dashboard, then attach them to the monitors or to a group. The script doesn’t copy them: they hold secrets.
  2. Swap the ping URL of each heartbeat in your cron jobs. Keep the same call: GET, POST and HEAD all count as a ping. See Heartbeat.
  3. Run both tools for a week and compare the alerts. With several regions, OKStatus only alerts when most of them agree: expect fewer one-off alerts.
  4. Recreate your status page and point your domain (CNAME) to OKStatus. See Status pages.
  5. Pause the UptimeRobot monitors, then close the account when you’re ready.
  • the uptime history: OKStatus starts counting on the day of the migration;
  • alert channels, status pages and maintenance windows;
  • credentials, headers and request bodies of HTTP monitors: add them in the dashboard;
  • types with no equivalent (UDP, third-party services) and heartbeats with a period over one day: the script lists them as skipped.