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.
What this tutorial does
Section titled “What this tutorial does”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.
Before you start
Section titled “Before you start”- 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.
1. How monitors map
Section titled “1. How monitors map”| UptimeRobot | OKStatus | Detail |
|---|---|---|
| HTTP(s) | HTTP | same URL, same method |
| Keyword | HTTP + keyword | “not exists” becomes keywordInvert |
| Ping | Ping | |
| Port | TCP | HTTP 80, HTTPS 443, FTP 21, SMTP 25, POP3 110, IMAP 143 or your custom port |
| Heartbeat | Heartbeat (push) | a new ping URL for your cron jobs; periods over one day are skipped |
| Paused | Paused | the monitor is created, then paused |
| Interval | Interval | raised to your plan’s minimum when needed, at most one day |
2. The script
Section titled “2. The script”Download uptimerobot_to_okstatus.py, or copy it:
#!/usr/bin/env python3"""Copy UptimeRobot monitors to OKStatus.
Reads your monitors with UptimeRobot's API (v2, read-only key is enough) andcreates them in OKStatus with its REST API (Full key, Max plan). Without--apply it only prints what it would create. Your UptimeRobot account isnever 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 jsonimport osimport reimport sysimport urllib.errorimport urllib.parseimport 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_typeMETHODS = {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()3. A dry run
Section titled “3. A dry run”export UPTIMEROBOT_API_KEY=ur…export OKSTATUS_REGIONS=fr-dc3,de-fra,nl-ams # regions of your planexport OKSTATUS_MIN_INTERVAL=30 # 300 on Free, 60 on Pro, 30 on Maxpython3 uptimerobot_to_okstatus.pyTO CREATE http Website -> https://example.comTO CREATE http API health -> https://api.example.com/healthTO CREATE tcp SMTP -> mail.example.comTO CREATE push Nightly backup -> nightly-backupTO 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.
4. The migration
Section titled “4. The migration”export OKSTATUS_API_KEY=oks_live_…python3 uptimerobot_to_okstatus.py --applyCREATED WebsiteCREATED API healthCREATED SMTPCREATED Nightly backup · new ping URL: https://backend.okstatus.eu/push/…CREATED Office router
5 created, 0 skipped, 0 failed.5. After the migration
Section titled “5. After the migration”- 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.
- Swap the ping URL of each heartbeat in your cron jobs. Keep the same call:
GET,POSTandHEADall count as a ping. See Heartbeat. - 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.
- Recreate your status page and point your domain (CNAME) to OKStatus. See Status pages.
- Pause the UptimeRobot monitors, then close the account when you’re ready.
What isn’t migrated
Section titled “What isn’t migrated”- 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.