-
-
Save dankrause/5585907 to your computer and use it in GitHub Desktop.
import requests | |
class HoverException(Exception): | |
pass | |
class HoverAPI(object): | |
def __init__(self, username, password): | |
params = {"username": username, "password": password} | |
r = requests.post("https://www.hover.com/api/login", params=params) | |
if not r.ok or "hoverauth" not in r.cookies: | |
raise HoverException(r) | |
self.cookies = {"hoverauth": r.cookies["hoverauth"]} | |
def call(self, method, resource, data=None): | |
url = "https://www.hover.com/api/{0}".format(resource) | |
r = requests.request(method, url, data=data, cookies=self.cookies) | |
if not r.ok: | |
raise HoverException(r) | |
if r.content: | |
body = r.json() | |
if "succeeded" not in body or body["succeeded"] is not True: | |
raise HoverException(body) | |
return body | |
# connect to the API using your account | |
client = HoverAPI("myusername", "mypassword") | |
# get details of a domains without DNS records | |
client.call("get", "domains") | |
# get all domains and DNS records | |
client.call("get", "dns") | |
# notice the "id" field of domains in response to the above calls - that's needed | |
# to address the domains individually, like so: | |
# get details of a specific domain without DNS records | |
client.call("get", "domains/dom123456") | |
# get DNS records of a specific domain: | |
client.call("get", "domains/dom123456/dns") | |
# create a new A record: | |
record = {"name": "mysubdomain", "type": "A", "content": "127.0.0.1"} | |
client.call("post", "domains/dom123456/dns", record) | |
# create a new SRV record | |
# note that content is "{priority} {weight} {port} {target}" | |
record = {"name": "mysubdomain", "type": "SRV", "content": "10 10 123 __service"} | |
client.call("post", "domains/dom123456/dns", record) | |
# create a new MX record | |
# note that content is "{priority} {host}" | |
record = {"name": "mysubdomain", "type": "MX", "content": "10 mail"} | |
client.call("post", "domains/dom123456/dns", record) | |
# notice the "id" field of DNS records in the above calls - that's | |
# needed to address the DNS records individually, like so: | |
# update an existing DNS record | |
client.call("put", "dns/dns1234567", {"content": "127.0.0.1"}) | |
# delete a DNS record: | |
client.call("delete", "dns/dns1234567") |
#!/usr/bin/python | |
""" | |
bulkhover.py 1.1 | |
This is a command-line script to import and export DNS records for a single | |
domain into or out of a hover account. | |
Usage: | |
bulkhover.py [options] (import|export) <domain> <dnsfile> | |
bulkhover.py (-h | --help) | |
bulkhover.py --version | |
Options: | |
-h --help Show this screen | |
--version Show version | |
-c --conf=<conf> Path to conf | |
-u --username=<user> Your hover username | |
-p --password=<pass> Your hover password | |
-f --flush Delete all existing records before importing | |
Examples: | |
The DNS file should have one record per line, in the format: | |
{name} {type} {content} | |
For example: | |
www A 127.0.0.1 | |
@ MX 10 example.com | |
Since the script output is in the same format as its input, you can use shell | |
pipelines to do complex operations. | |
Copy all DNS records from one domain to another: | |
bulkhover.py -c my.conf export example.com - | ./bulkhover.py -c my.conf -f import other.com - | |
Copy only MX records from one domain to another: | |
./bulkhover.py -c my.conf export foo.com - | awk '$2 == "MX" {print $0}' | ./bulkhover.py -c my.conf import bar.com - | |
To avoid passing your username and password in the command-line, you can use | |
a conf file that contains them instead: | |
[hover] | |
username=YOUR_USERNAME | |
password=YOUR_PASSWORD | |
""" | |
import ConfigParser | |
import docopt | |
import requests | |
import sys | |
class HoverException(Exception): | |
pass | |
class HoverAPI(object): | |
def __init__(self, username, password): | |
params = {"username": username, "password": password} | |
r = requests.post("https://www.hover.com/api/login", params=params) | |
if not r.ok or "hoverauth" not in r.cookies: | |
raise HoverException(r) | |
self.cookies = {"hoverauth": r.cookies["hoverauth"]} | |
def call(self, method, resource, data=None): | |
url = "https://www.hover.com/api/{0}".format(resource) | |
r = requests.request(method, url, data=data, cookies=self.cookies) | |
if not r.ok: | |
raise HoverException(r) | |
if r.content: | |
body = r.json() | |
if "succeeded" not in body or body["succeeded"] is not True: | |
raise HoverException(body) | |
return body | |
def import_dns(username, password, domain, filename, flush=False): | |
try: | |
client = HoverAPI(username, password) | |
except HoverException as e: | |
raise HoverException("Authentication failed") | |
if flush: | |
records = client.call("get", "domains/{0}/dns".format(domain))["domains"][0]["entries"] | |
for record in records: | |
client.call("delete", "dns/{0}".format(record["id"])) | |
print "Deleted {name} {type} {content}".format(**record) | |
domain_id = client.call("get", "domains/{0}".format(domain))["domain"]["id"] | |
if filename == "-": filename = "/dev/stdin" | |
with open(filename, "r") as f: | |
for line in f: | |
parts = line.strip().split(" ", 2) | |
record = {"name": parts[0], "type": parts[1], "content": parts[2]} | |
client.call("post", "domains/{0}/dns".format(domain), record) | |
print "Created {name} {type} {content}".format(**record) | |
def export_dns(username, password, domain, filename): | |
try: | |
client = HoverAPI(username, password) | |
except HoverException as e: | |
raise HoverException("Authentication failed") | |
records = client.call("get", "domains/{0}/dns".format(domain))["domains"][0]["entries"] | |
if filename == "-": filename = "/dev/stdout" | |
with open(filename, "w") as f: | |
for record in records: | |
f.write("{name} {type} {content}\n".format(**record)) | |
def main(args): | |
def get_conf(filename): | |
config = ConfigParser.ConfigParser() | |
config.read(filename) | |
items = dict(config.items("hover")) | |
return items["username"], items["password"] | |
if args["--conf"] is None: | |
if not all((args["--username"], args["--password"])): | |
print("You must specifiy either a conf file, or a username and password") | |
return 1 | |
else: | |
username, password = args["--username"], args["--password"] | |
else: | |
username, password = get_conf(args["--conf"]) | |
try: | |
if args["import"]: | |
import_dns(username, password, args["<domain>"], args["<dnsfile>"], args["--flush"]) | |
elif args["export"]: | |
export_dns(username, password, args["<domain>"], args["<dnsfile>"]) | |
except HoverException as e: | |
print "Unable to update DNS: {0}".format(e) | |
return 1 | |
if __name__ == "__main__": | |
version = __doc__.strip().split("\n")[0] | |
args = docopt.docopt(__doc__, version=version) | |
status = main(args) | |
sys.exit(status) |
#!/usr/bin/env python | |
""" | |
dynhover.py 1.2 | |
This tool will update an A record for given (sub)domain in your hover.com | |
with your IP, or an IP that you specify | |
Usage: | |
dynhover.py (-c <conf> | -u <user> -p <password>) <domain> | |
dynhover.py (-h | --help) | |
dynhover.py --version | |
Options: | |
-h --help Show this screen | |
--version Show version | |
-c --conf=<conf> Path to conf | |
-u --username=<user> Your hover username | |
-p --password=<pass> Your hover password | |
-i --ip=<ip> An IP to set (auto-detected by default) | |
""" | |
import ConfigParser | |
import docopt | |
import requests | |
import sys | |
class HoverException(Exception): | |
pass | |
class HoverAPI(object): | |
def __init__(self, username, password): | |
params = {"username": username, "password": password} | |
r = requests.post("https://www.hover.com/api/login", params=params) | |
if not r.ok or "hoverauth" not in r.cookies: | |
raise HoverException(r) | |
self.cookies = {"hoverauth": r.cookies["hoverauth"]} | |
def call(self, method, resource, data=None): | |
url = "https://www.hover.com/api/{0}".format(resource) | |
r = requests.request(method, url, data=data, cookies=self.cookies) | |
if not r.ok: | |
raise HoverException(r) | |
if r.content: | |
body = r.json() | |
if "succeeded" not in body or body["succeeded"] is not True: | |
raise HoverException(body) | |
return body | |
def get_public_ip(): | |
return requests.get("http://ifconfig.me/ip").content | |
def update_dns(username, password, fqdn, ip): | |
try: | |
client = HoverAPI(username, password) | |
except HoverException as e: | |
raise HoverException("Authentication failed") | |
dns = client.call("get", "dns") | |
dns_id = None | |
for domain in dns["domains"]: | |
if fqdn == domain["domain_name"]: | |
fqdn = "@.{domain_name}".format(**domain) | |
for entry in domain["entries"]: | |
if entry["type"] != "A": continue | |
if "{0}.{1}".format(entry["name"], domain["domain_name"]) == fqdn: | |
dns_id = entry["id"] | |
break | |
if dns_id is None: | |
raise HoverException("No DNS record found for {0}".format(fqdn)) | |
response = client.call("put", "dns/{0}".format(dns_id), {"content": my_ip}) | |
if "succeeded" not in response or response["succeeded"] is not True: | |
raise HoverException(response) | |
def main(args): | |
if args["--username"]: | |
username, password = args["--username"], args["--password"] | |
else: | |
config = ConfigParser.ConfigParser() | |
config.read(args["--conf"]) | |
items = dict(config.items("hover")) | |
username, password = items["username"], items["password"] | |
domain = args["<domain>"] | |
ip = args["--ip"] or get_public_ip() | |
try: | |
update_dns(username, password, domain, ip) | |
except HoverException as e: | |
print "Unable to update DNS: {0}".format(e) | |
return 1 | |
return 0 | |
if __name__ == "__main__": | |
version = __doc__.strip().split("\n")[0] | |
args = docopt.docopt(__doc__, version=version) | |
status = main(args) | |
sys.exit(status) |
#!/bin/bash | |
[[ $# -lt 3 ]] && echo "Usage: $0 USERNAME PASSWORD DNS_ID" | |
USERNAME=${1} | |
PASSWORD=${2} | |
DNS_ID=${3} | |
# find your DNS ID here: https://www.hover.com/api/domains/yourdomain.com/dns/ | |
# (replace "yourdomain.com" with your actual domain, and look for the record | |
# you want to change. The ID looks like: dns1234567) | |
IP=$(curl "http://ifconfig.me/ip" -s) | |
curl "https://www.hover.com/api/dns/${DNS_ID}" \ | |
-X PUT \ | |
-d "content=${IP}" \ | |
-s \ | |
-b <(curl "https://www.hover.com/api/login" \ | |
-X POST \ | |
-G \ | |
-d "username=${USERNAME}" \ | |
-d "password=${PASSWORD}" \ | |
-s \ | |
-o /dev/null \ | |
-c -) | |
echo | |
@barcoboy is this still working? Can you show the login curl please? I have no 2FA and keep getting locked out.
@wayneconnolly, here are the two curl calls I use to add an entry to my Hover DNS:
#Login
curl "https://www.hover.com/api/login" -H "Content-type: application/json" -X POST -d "{"password": "$PASSWORD", "username": "$USERNAME"}" -s -S -c $COOKIEJAR -o /dev/null
#Add to DNS
curl -k "https://www.hover.com/api/domains/mydomain.com/dns" -X POST -d "name=${NAME}" -d "type=TXT" -d "content=${DATA}" -s -S -b $COOKIEJAR
@wayneconnolly, here are the two curl calls I use to add an entry to my Hover DNS:
#Login curl "https://www.hover.com/api/login" -H "Content-type: application/json" -X POST -d "{"password": "$PASSWORD", "username": "$USERNAME"}" -s -S -c $COOKIEJAR -o /dev/null
#Add to DNS curl -k "https://www.hover.com/api/domains/mydomain.com/dns" -X POST -d "name=${NAME}" -d "type=TXT" -d "content=${DATA}" -s -S -b $COOKIEJAR
Thank you very much. I'll try this later today. I do note that the login is different so that's a positive thing. https://www.hover.com/signin/auth.json
. I emailed hover yesterday asking how to do it also. No idea if they will reply in a helpful manner.
UPDATE: I just tried with username password as you described and it gets a 404. A direct curl to that endpoint shows a 404 also. Could you please confirm on your end?
$ curl -k "https://www.hover.com/api/login"
<!DOCTYPE html>
<html>
<head>
<script src="https://browser.sentry-cdn.com/6.2.3/bundle.min.js" crossorigin="anonymous"></script>
<script src="https://browser.sentry-cdn.com/6.2.3/bundle.tracing.min.js" crossorigin="anonymous"></script>
<script>
if (window.Sentry) {
var release = "";
var username = "";
var admin = "";
Sentry.init({
dsn: "https://[email protected]/164872",
release,
ignoreErrors: [
"gCrWeb",
"hotjar",
"bat.bing.com",
// The rest of this list is taken from
// https://gist.github.com/impressiver/5092952
// which is linked from:
// https://docs.sentry.io/clients/javascript/tips/
"top.GLOBALS",
"originalCreateNotification",
"canvas.contentDocument",
"MyApp_RemoveAllHighlights",
"http://tt.epicplay.com",
"Can't find variable: ZiteReader",
"jigsaw is not defined",
"ComboSearch is not defined",
"http://loading.retry.widdit.com/",
"atomicFindClose",
"fb_xd_fragment",
"bmi_SafeAddOnload",
"EBCallBackMessageReceived",
"conduitPage",
],
whitelistUrls: [/www\.hover\.com/i],
integrations: [new Sentry.Integrations.BrowserTracing()],
});
if (username) {
Sentry.setUser({ username });
}
if (admin) {
Sentry.setContext("admin", {
username: admin,
});
}
}
</script>
<title>Domain Names | Buy Domains & Email At Hover.com</title>
<meta content='text/html; charset=UTF-8' http-equiv='content-type'>
<meta content='IE=Edge' http-equiv='X-UA-Compatible'>
<meta content='3CbaVvw-I7MlrmmmHz0bfbko7oMCW1mn2u65uWsWWB8' name='google-site-verification'>
<meta content='b6zle0gvjb63epbzegrrmbp4wwltnn' name='facebook-domain-verification'>
<meta content='width=device-width, initial-scale=1.0' name='viewport'>
<meta content='telephone=no' name='format-detection'>
<meta content='Find the perfect domain name for your idea at Hover. All domains come with industry-leading customer support and free WHOIS privacy. Name your passion today!' name='description'>
<meta content='Find the perfect domain name for your idea at Hover. All domains come with industry-leading customer support and free WHOIS privacy. Name your passion today!' property='og:description'>
<meta content='Domain Names | Buy Domains & Email At Hover.com' property='og:title'>
<meta content='/packs/src/application/images/home/og_hover-ff5e561a072494f142806a1ee8541fca.png' property='og:image'>
<link href='https://www.hover.com/api/login' rel='canonical'>
<link href='/site.webmanifest' rel='manifest'>
<link color='#229e87' href='/safari-pinned-tab.svg' rel='mask-icon'>
<link href='/apple-touch-icon-57x57.png' rel='apple-touch-icon' sizes='57x57'>
<link href='/apple-touch-icon-114x114.png' rel='apple-touch-icon' sizes='114x114'>
<link href='/apple-touch-icon-72x72.png' rel='apple-touch-icon' sizes='72x72'>
<link href='/apple-touch-icon-144x144.png' rel='apple-touch-icon' sizes='144x144'>
<link href='/apple-touch-icon-60x60.png' rel='apple-touch-icon' sizes='60x60'>
<link href='/apple-touch-icon-120x120.png' rel='apple-touch-icon' sizes='120x120'>
<link href='/apple-touch-icon-76x76.png' rel='apple-touch-icon' sizes='76x76'>
<link href='/apple-touch-icon-152x152.png' rel='apple-touch-icon' sizes='152x152'>
<link href='/apple-touch-icon-180x180.png' rel='apple-touch-icon' sizes='180x180'>
<link href='/favicon-196x196.png' rel='icon' sizes='196x196' type='image/png'>
<link href='/favicon-160x160.png' rel='icon' sizes='160x160' type='image/png'>
<link href='/favicon-96x96.png' rel='icon' sizes='96x96' type='image/png'>
<link href='/favicon-16x16.png' rel='icon' sizes='16x16' type='image/png'>
<link href='/favicon-32x32.png' rel='icon' sizes='32x32' type='image/png'>
<meta content='#229e87' name='msapplication-TileColor'>
<meta content='/mstile-144x144.png' name='msapplication-TileImage'>
<meta content='#229e87' name='theme-color'>
<link rel="stylesheet" media="all" href="/packs/fonts-fba7b87f80dc4ec5b49cfc53e3d92ff7.css" />
<link rel="stylesheet" media="all" href="/packs/application-3825aff153368bada4a3ba66c31869af.css" />
<link rel="stylesheet" media="all" href="/packs/hover_refresh-c7bcda836c7a2770625a7ecff139ed21.css" />
<link rel="stylesheet" media="all" href="https://fonts.googleapis.com/css?family=Open+Sans:400,300,600" />
<script src="/packs/manifest-35aff3e5d85edee906e3.js"></script>
</head>
<body class='has_new_styles'>
<script>dataLayer = [{}];</script>
<noscript><iframe src="//www.googletagmanager.com/ns.html?id=GTM-TDSBDF"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-TDSBDF');</script>
<header class='main__header'>
<div class='main__logo'>
<a href='/' title='Return to home page'>
<img alt="Hover Logo" src="/packs/src/application/images/common/hv-logo-2020-e535dce1f67e9a648643e23cac750002.svg" />
</a>
</div>
<nav class='main__nav'>
<div class='nav__primary'>
<a class='nav__link' href='/domains' title='Domains'>Domains</a>
<a class='nav__link' href='/email' title='Email'>Email</a>
<a class='nav__link' href='/about' title='About Us'>About Us</a>
<a class='nav__link' href='https://hover.blog' title='Blog'>Blog</a>
</div>
<div class='nav__secondary'>
<a class='nav__link' href='https://help.hover.com'>Help</a>
<a class='nav__link has-more' data-trigger='signin' href='/signin'>
Sign In
<i class='fas fa-chevron-down'></i>
</a>
<div class='signin__menu'>
<a class='signin__item' href='/signin'>
<div class='signin__heading'>Control Panel</div>
<p>View and manage domains, email, and features for your account.</p>
<i class='far fa-arrow-right'></i>
</a>
<a class='signin__item' href='https://mail.hover.com/'>
<div class='signin__heading'>Webmail</div>
<p>Access your email inbox from your web browser.</p>
<i class='far fa-arrow-right'></i>
</a>
<a class='signin__item' href='/signup'>Need an account? Sign up</a>
</div>
</div>
<div class='nav__cart' style='display: none'>
<a class='cart__button' href='/cart'>
<i class='fas fa-shopping-cart'></i>
<span class='cart__count'></span>
</a>
</div>
</nav>
<a class='mobile__menu-trigger' data-trigger='menu'>
<i class='far fa-bars'></i>
</a>
</header>
<div aria-hidden='true' class='mobile__menu'>
<a class='mobile__menu-trigger' data-trigger='menu'>
<i class='fal fa-times'></i>
</a>
<nav class='mobile__nav'>
<a class='nav__link' href='/domains'>Domains</a>
<a class='nav__link' href='/email'>Email</a>
<a class='nav__link' href='/about'>About Us</a>
<a class='nav__link' href='https://hover.blog'>Blog</a>
<div class='nav__separator'></div>
<a class='nav__link' href='https://help.hover.com'>Help</a>
<a class='nav__link' href='https://mail.hover.com/'>Webmail</a>
<a class='nav__link' href='/signin'>Sign In</a>
</nav>
</div>
<div id='your_account_menu'>
<ul>
<li class='title'>
Your Account
</li>
</ul>
</div>
<main>
<div class='grid_container' id='content'>
<div class='home error not_found'>
<section class='hero' style='padding-top: 40px;'>
<img src='/packs/src/application/images/404-456216b46583c9e46ecec75ca1c626cc.svg' width='580'>
<h1>is a great number...but not a page at Hover.com.</h1>
</section>
</div>
</div>
</main>
<footer class='main__footer'>
<div class='footer__top'>
<div class='footer__brand'>
<div class='main__logo'>
<a href='/'>
<img src='https://hover-assets.s3.ca-central-1.amazonaws.com/images/logo.svg'>
</a>
</div>
<div class='footer__social'>
<a aria-label='Twitter' href='https://twitter.com/hover'>
<i class='fab fa-twitter'></i>
</a>
<a aria-label='Facebook' href='https://www.facebook.com/hover'>
<i class='fab fa-facebook-f'></i>
</a>
<a aria-label='LinkedIn' href='https://www.linkedin.com/showcase/hover-domains-and-email/'>
<i class='fab fa-linkedin-in'></i>
</a>
<a aria-label='TikTok' href='https://www.tiktok.com/@hoverdomainnames'>
<i class='fab fa-tiktok'></i>
</a>
</div>
</div>
<div class='footer__column footer__column--products'>
<div class='footer__heading'>Products</div>
<a class='footer__link' href='/domains'>Domains</a>
<a class='footer__link indented' href='/transfer-in'>Transfer</a>
<a class='footer__link indented' href='/renew'>Renew</a>
<a class='footer__link indented' href='/domain-pricing'>Pricing</a>
<a class='footer__link' href='/email'>Email</a>
</div>
<div class='footer__column footer__column--company'>
<div class='footer__heading'>Company</div>
<a class='footer__link' href='/about'>About Us</a>
<a class='footer__link' href='https://hover.blog'>Blog</a>
<a class='footer__link' href='https://www.tucows.com/careers/'>Jobs</a>
<a class='footer__link' href='/affiliates'>Affiliates</a>
</div>
<div class='footer__column footer__column--support'>
<div class='footer__heading'>Support</div>
<a class='footer__link' href='https://help.hover.com'>Help Center</a>
<a class='footer__link' href='https://hoverstatus.com/'>Service Status</a>
</div>
<div class='footer__column'>
<div class='footer__heading'>Account</div>
<a class='footer__link' href='/signin'>Sign In</a>
<a class='footer__link' href='https://mail.hover.com'>Webmail</a>
</div>
</div>
<div class='footer__bottom'>
<div class='footer__legal'>
<a href='/tos'>Terms of Service</a>
<a href='/privacy'>Privacy Policy</a>
</div>
<div class='footer__copyright'>
Copyright © 2023 Hover
</div>
</div>
</footer>
<script src="/packs/jquery-eb76d5d3fc0228c47ac1.js"></script>
<script src="/packs/application-c3f1f2b2a53c1db41d28.js"></script>
<script src="/packs/hover_refresh-fd258324985771c547ed.js"></script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-4171338-2', 'auto');
ga('require', 'linkid', 'linkid.js');
ga('set', 'anonymizeIp', true);
ga('send', 'pageview');
</script>
</body>
</html>
Sorry for the delay in getting back to you... slipped through the cracks!
a 404 error is normal to that URL when doing a GET request... you have use the POST method. Make sure you are doing that by using the "-X POST" command line parameter with curl.
Sorry for the delay in getting back to you... slipped through the cracks!
a 404 error is normal to that URL when doing a GET request... you have use the POST method. Make sure you are doing that by using the "-X POST" command line parameter with curl.
Thank you - curl "https://www.hover.com/api/login" -H "Content-type: application/json" -X POST -d "{"password": "$PASSWORD", "username": "$USERNAME"}" -s -S -c $COOKIEJAR -o /dev/null
I was using -X POST
.
I contacted hover.com and it's definately been disabled even with 2FA
Lei (Hover Help Center)
Jul 6, 2023, 12:05 EDT
Hello!
Thanks for your reply - API is no longer usable to log in to Hover.
If using an API is critical to your setup, I may suggest our sister company, [OpenSRS.com](http://opensrs.com/).
Best,
My logins to hover.com are not 2FA enforced. I enabled 2FA a while ago, and as soon as I saw it broke my dynamic DNS updates, I turned it off. Just tried logging into the Hover website now and there was no prompt for 2FA, nor did it ask me to re-enable it or set it back up again. I am no longer using this code, but rather some calls to the Hover API with curl, and everything has continued to work fine ever since I turned 2FA off.